走进WPF之MVVM完整案例


学习WPF如果不学MVVM,仿佛缺少了灵魂。那什么是MVVM呢?为什么要学MVVM呢,本以一个简单的增删改查的小例子,简述MVVM的基本知识及如何通过进行MVVM架构的程序开发,仅供学习分享使用,如有不足之处,还请指正。

什么是MVVM?

MVVM是Model-View-ViewModel的简写。它本质上就是MVC (Model-View- Controller)的改进版。即模型-视图-视图模型。分别定义如下:

  • 【模型】指的是后端传递的数据。
  • 【视图】指的是所看到的页面。
  • 【视图模型】mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:
    • 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
    • 二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。

MVVM示意图如下所示:

安装MvvmLight插件

项目名称右键-->管理NuGet程序包-->搜索MvvmLight-->安装。如下所示:

 弹出接受许可证窗口,点击【接受】如下所示:

 MvvmLight安装成功后,自动引用需要的第三方库,并默认生成示例内容,有些不需要的需要删除,如下所示:

MVVM示例截图

主要通过MVVM实现数据的CRUD【增删改查】基础操作,如下所示:

 MVVM开发步骤

1. 创建Model层

本例主要是对学生信息的增删改查,所以创建Student模型类,如下所示:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace WpfApp3.Models
 8 {
 9     /// 
10     /// 学生类
11     /// 
12     public class Student
13     {
14         /// 
15         /// 唯一标识
16         /// 
17         public int Id { get; set; }
18 
19         /// 
20         /// 学生姓名
21         /// 
22         public string Name { get; set; }
23 
24         /// 
25         /// 年龄
26         /// 
27         public int Age { get; set; }
28 
29         /// 
30         /// 班级
31         /// 
32         public string Classes { get; set; }
33     }
34 }

2. 创建DAL层

为了简化示例,模拟数据库操作,构建基础数据,如下所示:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 using WpfApp3.Models;
 7 
 8 namespace WpfApp3.DAL
 9 {
10     public class LocalDb
11     {
12         private List students;
13 
14         public LocalDb() {
15             init();
16         }
17 
18         /// 
19         /// 初始化数据
20         /// 
21         private void init() {
22             students = new List();
23             for (int i = 0; i < 30; i++)
24             {
25                 students.Add(new Student()
26                 {
27                     Id=i,
28                     Name=string.Format("学生{0}",i),
29                     Age=new Random(i).Next(0,100),
30                     Classes=i%2==0?"一班":"二班"
31                 });
32             }
33         }
34 
35         /// 
36         /// 查询数据
37         /// 
38         /// 
39         public List Query()
40         {
41             return students;
42         }
43 
44         /// 
45         /// 按名字查询
46         /// 
47         /// 
48         /// 
49         public List QueryByName(string name)
50         {
51             return students.Where((t) => t.Name.Contains(name)).ToList();//FindAll((t) => t.Name.Contains(name));
52         }
53 
54         public Student QueryById(int Id)
55         {
56             var student = students.FirstOrDefault((t) => t.Id == Id);
57             if (student != null)
58             {
59                 return new Student() { 
60                     Id=student.Id,
61                     Name=student.Name,
62                     Age=student.Age,
63                     Classes=student.Classes
64                 };
65             }
66             return null;
67         }
68 
69 
70         /// 
71         /// 新增学生
72         /// 
73         /// 
74         public void AddStudent(Student student)
75         {
76             if (student != null)
77             {
78                 students.Add(student);
79             }
80         }
81 
82         /// 
83         /// 删除学生
84         /// 
85         /// 
86         public void DelStudent(int Id)
87         {
88             var student = students.FirstOrDefault((t) => t.Id == Id); //students.Find((t) => t.Id == Id);
89             if (student != null)
90             {
91                 students.Remove(student);
92             }
93             
94         }
95     }
96 
97 
98 }

3. 创建View层

View层与用户进行交互,用户数据的展示,及事件的响应。在本例中,View层主要有数据查询展示,新增及编辑页面。

在View层,主要是命令的绑定,及数据的绑定。

  1. 在DataGridTextColumn中通过Binding="{Binding Id}"的形式绑定要展示的列属性名。
  2. 在Button按钮上通过Command="{Binding AddCommand}"的形式绑定要响应的命令。
  3. 在TextBox文本框中通过Text="{Binding Search}"的形式绑定查询条件属性。

数据展示窗口,如下所示:

 1 <Window x:Class="WpfApp3.MainWindow"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 5         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 6         xmlns:local="clr-namespace:WpfApp3"
 7         mc:Ignorable="d"
 8         Title="MainWindow" Height="450" Width="800">
 9     <Grid>
10         <Grid.RowDefinitions>
11             <RowDefinition Height="80">RowDefinition>
12             <RowDefinition Height="*">RowDefinition>
13         Grid.RowDefinitions>
14         <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="5" VerticalAlignment="Center">
15             <TextBlock Text="姓名:" Margin="10" Padding="5">TextBlock>
16             <TextBox x:Name="sname" Text="{Binding Search}" Width="120" Margin="10" Padding="5">TextBox>
17             <Button x:Name="btnQuery" Content="查询" Margin="10" Padding="5" Width="80" Command="{Binding QueryCommand}">Button>
18             <Button x:Name="btnReset" Content="重置" Margin="10" Padding="5" Width="80" Command="{Binding ResetCommand}">Button>
19             <Button x:Name="btnAdd" Content="创建" Margin="10" Padding="5" Width="80"  Command="{Binding AddCommand}">Button>
20         StackPanel>
21         <DataGrid x:Name="dgInfo" Grid.Row="1" AutoGenerateColumns="False" CanUserAddRows="False" CanUserSortColumns="False" Margin="10" ItemsSource="{Binding GridModelList}">
22             <DataGrid.Columns>
23                 <DataGridTextColumn Header="Id" Width="100"  Binding="{Binding Id}">DataGridTextColumn>
24                 <DataGridTextColumn Header="姓名" Width="100" Binding="{Binding Name}">DataGridTextColumn>
25                 <DataGridTextColumn Header="年龄" Width="100" Binding="{Binding Age}">DataGridTextColumn>
26                 <DataGridTextColumn Header="班级" Width="100" Binding="{Binding Classes}">DataGridTextColumn>
27                 <DataGridTemplateColumn Header="操作" Width="*">
28                     <DataGridTemplateColumn.CellTemplate>
29                         <DataTemplate>
30                             <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
31                                 <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>
32                                 <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>
33                             StackPanel>
34                         DataTemplate>
35                     DataGridTemplateColumn.CellTemplate>
36                 DataGridTemplateColumn>
37             DataGrid.Columns>
38         DataGrid>
39     Grid>
40 Window>

新增及编辑页面,如下所示:

 1 <Window x:Class="WpfApp3.Views.StudentWindow"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 5         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 6         xmlns:local="clr-namespace:WpfApp3.Views"
 7         mc:Ignorable="d"
 8         Title="StudentWindow" Height="440" Width="500" AllowsTransparency="False" WindowStartupLocation="CenterScreen" WindowStyle="None">
 9     <Grid>
10         <Grid.RowDefinitions>
11             <RowDefinition Height="60">RowDefinition>
12             <RowDefinition>RowDefinition>
13             <RowDefinition Height="60">RowDefinition>
14         Grid.RowDefinitions>
15         <TextBlock FontSize="30" Margin="10">修改学生信息TextBlock>
16         <StackPanel Grid.Row="1" Orientation="Vertical">
17             <TextBlock FontSize="20" Margin="10" Padding="5">姓名TextBlock>
18             <TextBox x:Name="txtName" FontSize="20"  Padding="5" Text="{Binding Model.Name}">TextBox>
19             <TextBlock FontSize="20" Margin="10"  Padding="5">年龄TextBlock>
20             <TextBox x:Name="txtAge" FontSize="20"  Padding="5" Text="{Binding Model.Age}">TextBox>
21             <TextBlock FontSize="20" Margin="10"  Padding="5">班级TextBlock>
22             <TextBox x:Name="txtClasses" FontSize="20"  Padding="5" Text="{Binding Model.Classes}">TextBox>
23         StackPanel>
24         <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
25             <Button x:Name="btnSave" Content="保存" Margin="10" FontSize="20" Width="100" Click="btnSave_Click" >Button>
26             <Button x:Name="btnCancel" Content="取消" Margin="10" FontSize="20" Width="100" Click="btnCancel_Click" >Button>
27         StackPanel>
28     Grid>
29 Window>

3. 创建ViewModel层

ViewModel层是MVVM的核心所在,起到承上启下的作用。ViewModel需要继承GalaSoft.MvvmLight.ViewModelBase基类。

ViewModel中属性实现数据的绑定,命令实现用户交互的响应。如下所示:

  1 using GalaSoft.MvvmLight;
  2 using GalaSoft.MvvmLight.Command;
  3 using System.Collections.Generic;
  4 using System.Collections.ObjectModel;
  5 using System.Linq;
  6 using System.Windows;
  7 using WpfApp3.DAL;
  8 using WpfApp3.Models;
  9 using WpfApp3.Views;
 10 
 11 namespace WpfApp3.ViewModel
 12 {
 13     /// 
 14     /// 
 15     /// 
 16     public class MainViewModel : ViewModelBase
 17     {
 18         #region 属性及构造函数
 19 
 20         private LocalDb localDb;
 21 
 22         private ObservableCollection gridModelList;
 23 
 24         public ObservableCollection GridModelList
 25         {
 26             get { return gridModelList; }
 27             set
 28             {
 29                 gridModelList = value;
 30                 RaisePropertyChanged();
 31             }
 32         }
 33 
 34         /// 
 35         /// 查询条件
 36         /// 
 37         private string search;
 38 
 39         public string Search
 40         {
 41             get { return search; }
 42             set
 43             {
 44                 search = value;
 45                 RaisePropertyChanged();
 46             }
 47         }
 48 
 49 
 50         /// 
 51         /// 
 52         /// 
 53         public MainViewModel()
 54         {
 55             localDb = new LocalDb();
 56             QueryCommand = new RelayCommand(this.Query);
 57             ResetCommand = new RelayCommand(this.Reset);
 58             EditCommand = new RelayCommand<int>(this.Edit);
 59             DeleteCommand = new RelayCommand<int>(this.Delete);
 60             AddCommand = new RelayCommand(this.Add);
 61         }
 62 
 63         #endregion
 64 
 65         #region command
 66 
 67         /// 
 68         /// 查询命令
 69         /// 
 70         public RelayCommand QueryCommand { get; set; }
 71 
 72         /// 
 73         /// 重置命令
 74         /// 
 75         public RelayCommand ResetCommand { get; set; }
 76 
 77         /// 
 78         /// 编辑
 79         /// 
 80         public RelayCommand<int> EditCommand { get; set; }
 81 
 82         /// 
 83         /// 删除
 84         /// 
 85         public RelayCommand<int> DeleteCommand { get; set; }
 86 
 87         /// 
 88         /// 新增
 89         /// 
 90         public RelayCommand AddCommand { get; set; }
 91 
 92         #endregion
 93 
 94         public void Query()
 95         {
 96             List students;
 97             if (string.IsNullOrEmpty(search))
 98             {
 99                 students = localDb.Query();
100             }
101             else
102             {
103                 students = localDb.QueryByName(search);
104             }
105 
106             GridModelList = new ObservableCollection();
107             if (students != null)
108             {
109                 students.ForEach((t) =>
110                 {
111                     GridModelList.Add(t);
112                 });
113             }
114         }
115 
116         /// 
117         /// 重置
118         /// 
119         public void Reset()
120         {
121             this.Search = string.Empty;
122             this.Query();
123         }
124 
125         /// 
126         /// 编辑
127         /// 
128         /// 
129         public void Edit(int Id)
130         {
131             var model = localDb.QueryById(Id);
132             if (model != null)
133             {
134                 StudentWindow view = new StudentWindow(model);
135                 var r = view.ShowDialog();
136                 if (r.Value)
137                 {
138                     var newModel = GridModelList.FirstOrDefault(t => t.Id == model.Id);
139                     if (newModel != null)
140                     {
141                         newModel.Name = model.Name;
142                         newModel.Age = model.Age;
143                         newModel.Classes = model.Classes;
144                     }
145                     this.Query();
146                 }
147             }
148         }
149 
150         /// 
151         /// 删除
152         /// 
153         /// 
154         public void Delete(int Id)
155         {
156             var model = localDb.QueryById(Id);
157             if (model != null)
158             {
159                 var r = MessageBox.Show($"确定要删除吗【{model.Name}】?","提示",MessageBoxButton.YesNo);
160                 if (r == MessageBoxResult.Yes)
161                 {
162                     localDb.DelStudent(Id);
163                     this.Query();
164                 }
165             }
166         }
167 
168         /// 
169         /// 新增
170         /// 
171         public void Add()
172         {
173             Student model = new Student();
174             StudentWindow view = new StudentWindow(model);
175             var r = view.ShowDialog();
176             if (r.Value)
177             {
178                 model.Id = GridModelList.Max(t => t.Id) + 1;
179                 localDb.AddStudent(model);
180                 this.Query();
181             }
182         }
183     }
184 }

4. 数据上下文

当各个层分别创建好后,那如何关联起来呢?答案就是DataContext【数据上下文】。

查询页面上下文,如下所示:

 1 namespace WpfApp3
 2 {
 3     /// 
 4     /// MainWindow.xaml 的交互逻辑
 5     /// 
 6     public partial class MainWindow : Window
 7     {
 8         public MainWindow()
 9         {
10             InitializeComponent();
11             MainViewModel viewModel = new MainViewModel();
12             viewModel.Query();
13             this.DataContext = viewModel;
14         }
15     }
16 }

新增页面上下文,如下所示:

 1 namespace WpfApp3.Views
 2 {
 3     /// 
 4     /// StudentWindow.xaml 的交互逻辑
 5     /// 
 6     public partial class StudentWindow : Window
 7     {
 8         public StudentWindow(Student student)
 9         {
10             InitializeComponent();
11             this.DataContext = new
12             {
13                 Model = student
14             };
15         }
16 
17         private void btnSave_Click(object sender, RoutedEventArgs e)
18         {
19             this.DialogResult = true;
20         }
21 
22         private void btnCancel_Click(object sender, RoutedEventArgs e)
23         {
24             this.DialogResult = false;
25         }
26     }
27 }

总结

MVVM具有低耦合,可重用,可测试,独立开发的优点,核心要素就两个:

  • 属性发生变化时的通知,即可达到数据的实时更新。
  • 命令是实现用户与程序之间数据和算法的桥梁。

备注

本文作为MVVM的简单入门示例,旨在抛砖引玉,一起学习,共同进步。如果对WPF的其他入门知识,不是很了解,可以参考其他博文。

玉楼春·别后不知君远近

欧阳修 〔宋代〕

别后不知君远近,触目凄凉多少闷。渐行渐远渐无书,水阔鱼沉何处问。
夜深风竹敲秋韵,万叶千声皆是恨。故攲单枕梦中寻,梦又不成灯又烬。注:攲(yǐ)