走进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 Liststudents; 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层,主要是命令的绑定,及数据的绑定。
- 在DataGridTextColumn中通过Binding="{Binding Id}"的形式绑定要展示的列属性名。
- 在Button按钮上通过Command="{Binding AddCommand}"的形式绑定要响应的命令。
- 在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ǐ)