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是在MVC和MVP的基础上,进行了优化,弥补了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逻辑、命令、事件和对模型(Model)的引用。
在MVVM中,ViewModel不负责更新UI中显示的数据,因为WPF提供了数据绑定引擎,所以在ViewModel只需要处理逻辑,而不用去操作UI。
为了实现这一点,ViewModel必须实现INotifyPropertyChanged 接口并触发PropertyEvent事件。
本质上来说,这里是使用了观察者模式(一种设计模式),View是ViewModel的观察者,因此一旦ViewModel发生变化,UI就会自动更新。
说明:观察者模式可以访问以下链接了解:监视程序设计模式 - .NET | Microsoft Learn
在前面的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(关注点分离),代码的结构将会更清晰,同时也会提升可维护性。
MVVM所带来的一些缺点
1、缺乏官方的资料支持
虽然微软有Prism(以前由微软维护,现在由社区维护)/CommunityToolkit.Mvvm等MVVM相关的包,但在Microsoft Learn上并不能直接找到关于MVVM模式开发的介绍及使用文档。
2、开发模式的转变需要适应
对于习惯了Winform的开发人员来说,这种新的开发模式需要去适应。
3、对WPF的掌握程度要求较高
在项目中使用MVVM模式,需要熟练掌握WPF的绑定、命令、转换器、依赖属性、模板、样式等特性。
4、学习周期问题
理解 MVVM开发模式不难,短时间学习后也能运用到项目,但是要在企业应用中有效地实现该模式仍需要一段很长的学习时间。
何时使用MVVM模式
MVVM是UI层的一种设计模式,设计模式的诞生是为了解决问题,所以最核心的点在解决问题。在解决问题的基础上,我们再谈架构,谈可维护性。
关于何时使用MVVM模式,这个没有硬性要求,个人看来,在自己能力范围内,做自己力所能及的事情就好了。
比如当我们接到一个开发任务,领导下达的指令是快速完成。对于这种情况,我们可以直接使用传统操作控件的模式去进行开发,必要时,UI代码和业务代码混在一起也是可以的。相间相对充裕点,可以将业务逻辑和UI操作做一些隔离,使代码的层次更清晰。
又或者,我们对MVVM还不是很熟练,开发起来有一定的难度,我们也可以暂时先不采用MVVM开发模式。
对于长期项目,或者稍微偏大一点的项目,这种情况下,建议采用MVVM模式开发,并在前期建议做好充足的准备,进行统一的规划。
写在最后的话
我记得我当初学习WPF MVVM模式开发比较困难的地方就是,理解 了MVVM这种开发模式,但是却不能很好的和WPF结合起来。
总结了一下,原因有以下几点:
1、没有成套的资料可以供参考学习,网上流传的资料大多都是针对 MVVM模式,但是对于实现这种模式需要掌握的技术点却并没有很好的说明。
2、模式的转变一下难以适应。
随着学习的深入,我逐渐掌握了这种开发模式,所以才有这一系列的文章。
在后面的文章中,我会将MVVM开发中需要了解的点点滴滴都尽量覆盖到。本人能力有限,在文章中如果有错误之处,还恳请读者小伙伴可以帮忙指出,我会及时修正。