WPF MVVM入门系列教程(一、MVVM模式介绍)
前言
还记得早些年刚工作的那会,公司的产品从Delphi转成了WPF(再早些年是mfc)。当时大家也是处于一个对WPF探索的阶段,所以有很多概念都不是非常清楚。
但是大家都想堆技术,就提出使用MVVM,我那会是第一次听到MVVM,在网上看了一些资料后,也难以理解,后面也是硬着头皮在写。
有意思的是其它年资高一点的同事,他们也不能很好的运行MVVM模式进行开发,写着写着,都变成了Code-Behind模式。
后面工作几年后,对WPF的一些技术点都逐渐熟练,再去学习MVVM模式的开发就变得相对容易 一些。
写本文的目的除了对自己 使用MVVM开发经验的一些总结 ,更多的是为了帮助有需要的小伙伴,特别是刚接触MVVM模式开发的。
一直都想写这样一个系列的文章,但是没有付诸行动,人到中年,很多事都身不由己。
一种惯用的模式
从Visual Basic, 到Delphi, Visual FoxPro,.NET Windows Forms, ASP.NET WebForms等,我们都是使用代码直接去操作界面元素。
这种代码也可以称之为意大利面条式代码(spaghetticode),指的是和UI捆绑在一起并且具有低内聚力的类和方法。
让我们先看看下面的示例
假设我们在界面放置一个文本显示 、 一个文本框和一个按钮控件,当按钮点击时,弹框显示文本框里的内容
Winform示例
首先我们使用Visual Studio创建一个Winform工程,从工具箱中拖入Label,TextBox和Button,界面布局如下:
设计器自动生成的界面代码
Form1.Designer.cs
1 namespace WinformDemo 2 { 3 partial class Form1 4 { 5 /// <summary> 6 /// Required designer variable. 7 /// </summary> 8 private System.ComponentModel.IContainer components = null; 9 10 /// <summary> 11 /// Clean up any resources being used. 12 /// </summary> 13 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> 14 protected override void Dispose(bool disposing) 15 { 16 if (disposing && (components != null)) 17 { 18 components.Dispose(); 19 } 20 base.Dispose(disposing); 21 } 22 23 #region Windows Form Designer generated code 24 25 /// <summary> 26 /// Required method for Designer support - do not modify 27 /// the contents of this method with the code editor. 28 /// </summary> 29 private void InitializeComponent() 30 { 31 label1 = new Label(); 32 textBox1 = new TextBox(); 33 button1 = new Button(); 34 SuspendLayout(); 35 // 36 // label1 37 // 38 label1.AutoSize = true; 39 label1.Location = new Point(155, 157); 40 label1.Name = "label1"; 41 label1.Size = new Size(56, 17); 42 label1.TabIndex = 0; 43 label1.Text = "输入内容"; 44 // 45 // textBox1 46 // 47 textBox1.Location = new Point(229, 151); 48 textBox1.Name = "textBox1"; 49 textBox1.Size = new Size(100, 23); 50 textBox1.TabIndex = 1; 51 // 52 // button1 53 // 54 button1.Location = new Point(195, 200); 55 button1.Name = "button1"; 56 button1.Size = new Size(75, 23); 57 button1.TabIndex = 2; 58 button1.Text = "获取输入"; 59 button1.UseVisualStyleBackColor = true; 60 button1.Click += button1_Click; 61 // 62 // Form1 63 // 64 AutoScaleDimensions = new SizeF(7F, 17F); 65 AutoScaleMode = AutoScaleMode.Font; 66 ClientSize = new Size(532, 357); 67 Controls.Add(button1); 68 Controls.Add(textBox1); 69 Controls.Add(label1); 70 Name = "Form1"; 71 Text = "Form1"; 72 ResumeLayout(false); 73 PerformLayout(); 74 } 75 76 #endregion 77 78 private Label label1; 79 private TextBox textBox1; 80 private Button button1; 81 } 82 }
后台逻辑代码
Form1.cs
1 namespace WinformDemo 2 { 3 public partial class Form1 : Form 4 { 5 public Form1() 6 { 7 InitializeComponent(); 8 } 9 10 private void button1_Click(object sender, EventArgs e) 11 { 12 MessageBox.Show(this.textBox1.Text); 13 } 14 } 15 }
运行效果
WPF示例
首先我们使用Visual Studio创建一个WPF工程,在XAML中进行布局,放置Label,TextBox和Button控件
MainWindow.xaml
1 <Window x:Class="WpfDemo.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:WpfDemo" 7 mc:Ignorable="d" 8 Title="MainWindow" Height="450" Width="800"> 9 <Grid> 10 <Label Content="输入内容" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="-100,0,0,0"></Label> 11 <TextBox Width="200" Name="tbox" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="384,0,0,0"></TextBox> 12 <Button Content="获取输入" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="400,252,0,0" Click="Button_Click"/> 13 </Grid> 14 </Window>
MainWindow.xaml.cs
1 using System.Windows; 2 3 namespace WpfDemo 4 { 5 public partial class MainWindow : Window 6 { 7 public MainWindow() 8 { 9 InitializeComponent(); 10 } 11 12 private void Button_Click(object sender, RoutedEventArgs e) 13 { 14 MessageBox.Show(this.tbox.Text); 15 } 16 } 17 }
WPF MVVM示例
在正式开始学习MVVM之前,我们先通过这个示例简单感受一下。
MainWindow.xaml
1 <Window x:Class="WpfMVVMDemo.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:WpfMVVMDemo" 7 mc:Ignorable="d" 8 Title="MainWindow" Height="450" Width="800"> 9 <Grid> 10 <Label Content="输入内容" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="-100,0,0,0"></Label> 11 <TextBox Width="200" Text="{Binding InputText}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="384,0,0,0"></TextBox> 12 <Button Content="获取输入" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="400,252,0,0" Command="{Binding GetInputCommand}"/> 13 </Grid> 14 </Window>
可以看到代码中加粗的部分,这里就是对文本框的文本和按钮的命令进行绑定。现在不理解没有关系,我们接着往下看。
此时我们不写任何后台代码,程序 也能运行起来,只是单击按钮没有反应。
我们为MainWindow增加一个ViewModel
MainWindowViewModel.cs
1 using System.ComponentModel; 2 using System.Windows; 3 4 namespace WpfMVVMDemo 5 { 6 public class MainWindowViewModel : INotifyPropertyChanged 7 { 8 public event PropertyChangedEventHandler? PropertyChanged; 9 10 private string inputText; 11 12 public string InputText 13 { 14 get => this.inputText; 15 set 16 { 17 inputText = value; 18 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("InputText")); 19 } 20 } 21 22 public DelegateCommand GetInputCommand { get; set; } 23 24 public MainWindowViewModel() 25 { 26 GetInputCommand = new DelegateCommand(GetInput); 27 } 28 29 30 private void GetInput() 31 { 32 MessageBox.Show(InputText); 33 } 34 } 35 }
将ViewModel绑定到界面的数据上下文(DataContext)
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 this.DataContext = new MainWindowViewModel(); 8 } 9 }
此时再执行,点击按钮,就可以弹框显示文本框的内容。
MVVM模式
MVVM的全称是:Model(模型)- View(视图)- ViewModel(模型视图)。MVVM是表现层(UI层)常用的一种设计模式。
在企业软件开发过程中,关注点分离(Separation of Concerns, SOC)是一个核心原则,它提供了许多好处,例如增强可维护性和提高系统的灵活性。而MVVM已成为在用户界面中实现 SoC 的惯用模式。
我记得刚接触MVVM的那会,在网上查的资料都会将MVVM与MVC、MVP模式进行对比。
我觉得对于初学者来说,这个不是必须的,在引入较多的概念后,反而会引起混淆。
MVVM模式的整体结构如下所示
下面大概介绍一下各层的作用,在后面的文章中,会进行详细讲解
Model层:
模型是代表业务概念的实体;它可以是任何实体,从简单的客户实体到复杂的数据实体。
例如一个学生类
1 public class Student 2 { 3 public int Id { get; set; } 4 5 public string Name { get; set; } 6 7 public DateTime Birthday { get; set; } 8 9 public int Score { get; set; } 10 }
View层:
View是负责渲染Model的图形控件或控件集。屏幕上的View可以是WPF窗口、WPF页,也可以是一个数据模板。
在MVVM模式下,View仍然负责显示数据、收集用户输入并传递它,但是现在它被传递给ViewModel层。
ViewModel层:
ViewModel包含UI逻辑、命令、事件和对模型的引用。
在MVVM中,ViewModel不负责更新UI中显示的数据,因为WPF提供了数据绑定引擎,所以在ViewModel只需要处理逻辑,而不用去操作UI。
为了实现这一点,ViewModel必须实现INotifyPropertyChanged 接口并触发PropertyEvent事件。
本质上来说,这里是使用了观察者模式(一种设计模式),View是ViewModel的观察者,因此一旦ViewModel发生变化,UI就会自动更新。
在前面的MVVM示例代码中,我们新建了一个InputText属性,并在值更改时,触发PropertyEvent事件。
然后我们将InputText绑定到文本框上,当文本框的值发生变化时,WPF的(Binding)绑定功能,会更新绑定的属性值,也就是InputText。另一方面,当InputText属性值发生更改时,View是ViewModel的观察者,检查到值的变化,会更新UI。
除了InputText属性,还增加了一个GetInputCommand命令,将它绑定到Button的Command上,当按钮点击时,就会执行这个命令。
阅读到这里,有些小伙伴可能会有很多疑问,但是我们可以先不去看这些技术细节,只在意这样一种开发模式,后面我会将WPF MVVM开发中涉及的各个技术点进行详细讲解。
MVVM所带来的一些优点
1、良好的测试性
传统模式下,代码的可测试性较差,因为整个代码与UI紧密耦合,需要通过UI来驱动应用程序逻辑的事件,比如点击。
使用MVVM模式后,因为跟UI相关的值封装成了一个属性,事件封装成了命令,所以我们就可以单独进行单元测试,而不需要UI。
2、良好的扩展性和代码重用
由于跟UI解耦,所以ViewModel中的逻辑都可以单独进行开发,这就具备了良好的扩展性和代码重用功能。
我们可以将组件构建到单独的DLL中,还可以通过替换组件来提供不同的行为。
3、良好的代码结构以及可维护性
使用MVVM模式后,实现了SOC(关注点分离),代码的结构将会更清晰,同时也会提升可维护性。