• 深入理解WPF中MVVM的设计思想


    近些年来,随着WPF在生产,制造,工业控制等领域应用越来越广发,很多企业对WPF开发的需求也逐渐增多,使得很多人看到潜在机会,不断从Web,WinForm开发转向了WPF开发,但是WPF开发也有很多新的概念及设计思想,如:数据驱动,数据绑定,依赖属性,命令,控件模板,数据模板,MVVM等,与传统WinForm,ASP.NET WebForm开发,有很大的差异,今天就以一个简单的小例子,简述WPF开发中MVVM设计思想及应用。

    为什么要用MVVM?

    传统的WinForm开发,一般采用事件驱动,即用户点击事件,触发对应的事件,并在事件中通过唯一标识符获取页面上用户输入的数据,然后进行业务逻辑处理。这样做会有一个弊端,就是用户输入(User Interface)和业务逻辑(Business)是紧密耦合在一起的,无法做到分离,随着项目的业务不断复杂化,这种高度耦合的弊端将会越来越明显。并且会出现分工不明确(如:后端工程师,前端UI),工作无法拆分的现象。所以分层(如:MVC,MVVM),可测试(Unit Test),前后端分离,就成为必须要面对的问题。而今天要讲解的MVVM设计模式,就非常好的解决了我们所面临的问题。

    什么是MVVM?

    MVVM即模型(Model)-视图(View)-视图模型(ViewModel) ,是用于解耦 UI 代码和非 UI 代码的 设计模式。 借助 MVVM,可以在 XAML 中以声明方式定义 UI,将 UI使用数据绑定标到包含数据和命令的其他层。 数据绑定提供数据和结构的松散耦合,使 UI 和链接的数据保持同步,同时可以将用户输入路由到相应的命令。具体如下图所示:

    如上图所示:

    1. View(用户页面),主要用于向使用者展示信息,并接收用户输入的信息(数据绑定),及响应用户的操作(Command)。
    2. ViewModel(用户视图业务逻辑),主要处理客户请求,以及数据呈现。
    3. Model数据模型,作为存储数据的载体,是一个个的具体的模型类,通过ViewModel进行调用。但是在小型项目中,Model并不是必须的
    4. IService(数据接口),数据访问服务,用于获取各种类型数据的服务。数据的形式有很多种,如网络数据,本地数据,数据库数据,但是在ViewModel调用时,都统一封装成了Service。在小型项目中,IService数据接口也并不是必须的,不属于MVVM的范畴
    5. 在上图中,DataBase,Network,Local等表示不同的数据源形式,并不属于MVVM的范畴

    前提条件

    要实现MVVM,首先需要满足两个条件:

    1. 属性变更通知,在MVVM思想中,由WinForm的事件驱动,转变成了数据驱动。在C#中,普通的属性,并不具备变更通知功能,要实现变更通知功能,必须要实现INotifyPropertyChanged接口。
    2. 绑定命令,在WPF中,为了解决事件响应功能之间的耦合,提出了绑定命令思想,即命令可以绑定的方式与控件建立联系。绑定命令必须实现ICommand接口。

    在上述两个条件都满足后,如何将ViewModel中的具备变更通知的属性和命令,与View中的控件关联起来呢?答案就是绑定(Binding)

    当View层的数据控件和具备通知功能的属性进行Binding后,Binging就会自动侦听来自接口的PropertyChanged事件。进而达到数据驱动UI的效果,可谓【一桥飞架南北,天堑变通途】。

    MVVM实例

    为了进一步感受MVVM的设计思想,验证上述的理论知识,以实例进行说明。本实例的项目架构如下所示:

    MVVM核心代码

    1. 具备通知功能的属性

    首先定义一个抽象类ObservableObject,此接口实现INotifyPropertyChanged接口,如下所示:

    1. using System.ComponentModel;
    2. using System.Runtime.CompilerServices;
    3. namespace DemoMVVM.Core
    4. {
    5. ///
    6. /// 可被观测的类
    7. ///
    8. public abstract class ObservableObject : INotifyPropertyChanged
    9. {
    10. ///
    11. /// 属性改变事件
    12. ///
    13. public event PropertyChangedEventHandler? PropertyChanged;
    14. ///
    15. /// 属性改变触发方法
    16. ///
    17. /// 属性名称
    18. protected void RaisePropertyChanged([CallerMemberName]string propertyName=null)
    19. {
    20. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    21. }
    22. ///
    23. /// 设置属性值,如果发生改变,则调用通知方法
    24. ///
    25. ///
    26. ///
    27. ///
    28. ///
    29. ///
    30. protected bool SetProperty<T>(ref T target,T value, [CallerMemberName] string propertyName = null)
    31. {
    32. if (EqualityComparer.Default.Equals(target, value))
    33. {
    34. return false;
    35. }
    36. else
    37. {
    38. target=value;
    39. RaisePropertyChanged(propertyName);
    40. return true;
    41. }
    42. }
    43. }
    44. }

     

    注意:上述SetProperty主要用于将普通属性,变为具备通知功能的属性。

    然后定义一个ViewMode基类,继承自ObservableObject,以备后续扩展,如下所示:

    1. namespace DemoMVVM.Core
    2. {
    3. ///
    4. /// ViewModel基类,继承自ObservableObject
    5. ///
    6. public abstract class ViewModelBase:ObservableObject
    7. {
    8. }
    9. }

     

    2. 具备绑定功能的命令

    首先定义一个DelegateCommand,实现ICommand接口,如下所示:

    1. namespace DemoMVVM.Core
    2. {
    3. public class DelegateCommand : ICommand
    4. {
    5. private Action<object> execute;
    6. private Predicate<object> canExecute;
    7. public event EventHandler? CanExecuteChanged;
    8. public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    9. {
    10. if (execute == null)
    11. {
    12. throw new ArgumentNullException("execute 不能为空");
    13. }
    14. this.execute = execute;
    15. this.canExecute = canExecute;
    16. }
    17. public DelegateCommand(Action<object> execute):this(execute,null)
    18. {
    19. }
    20. public bool CanExecute(object? parameter)
    21. {
    22. return canExecute?.Invoke(parameter)!=false;
    23. }
    24. public void Execute(object? parameter)
    25. {
    26. execute?.Invoke(parameter);
    27. }
    28. }
    29. }

     

    注意,DelegateCommand的构造函数,接收两个参数,一个是Execute(干活的),一个是CanExecute(判断是否可以干活的)

    MVVM应用代码

    本实例主要实现两个数的运算。如加,减,乘,除等功能。

    首先定义ViewModel,继承自ViewModelBase,主要实现具备通知功能的属性和命令,如下所示:

    1. using DemoMVVM.Core;
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Runtime;
    6. using System.Text;
    7. using System.Threading.Tasks;
    8. namespace DemoMVVM
    9. {
    10. public class MainWindowViewModel:ViewModelBase
    11. {
    12. #region 属性及构造函数
    13. private double leftNumber;
    14. public double LeftNumber
    15. {
    16. get { return leftNumber; }
    17. set { SetProperty(ref leftNumber , value); }
    18. }
    19. private double rightNumber;
    20. public double RightNumber
    21. {
    22. get { return rightNumber; }
    23. set { SetProperty(ref rightNumber , value); }
    24. }
    25. private double resultNumber;
    26. public double ResultNumber
    27. {
    28. get { return resultNumber; }
    29. set { SetProperty(ref resultNumber , value); }
    30. }
    31. public MainWindowViewModel()
    32. {
    33. }
    34. #endregion
    35. #region 命令
    36. private DelegateCommand operationCommand;
    37. public DelegateCommand OperationCommand
    38. {
    39. get {
    40. if (operationCommand == null)
    41. {
    42. operationCommand = new DelegateCommand(Operate);
    43. }
    44. return operationCommand; }
    45. }
    46. private void Operate(object obj)
    47. {
    48. if(obj == null)
    49. {
    50. return;
    51. }
    52. var type=obj.ToString();
    53. switch (type)
    54. {
    55. case "+":
    56. this.ResultNumber = this.LeftNumber + this.RightNumber;
    57. break;
    58. case "-":
    59. this.ResultNumber = this.LeftNumber - this.RightNumber;
    60. break;
    61. case "*":
    62. this.ResultNumber = this.LeftNumber * this.RightNumber;
    63. break;
    64. case "/":
    65. if (this.RightNumber == 0)
    66. {
    67. this.ResultNumber = 0;
    68. }
    69. else
    70. {
    71. this.ResultNumber = this.LeftNumber / this.RightNumber;
    72. }
    73. break;
    74. }
    75. }
    76. #endregion
    77. }
    78. }

     

     创建视图,并在视图中进行数据绑定,将ViewModel和UI关联起来,如下所示:

    1. <Window x:Class="DemoMVVM.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:DemoMVVM"
    7. mc:Ignorable="d"
    8. Title="MVVM示例" Height="350" Width="600">
    9. <Grid>
    10. <Grid.ColumnDefinitions>
    11. <ColumnDefinition>ColumnDefinition>
    12. <ColumnDefinition>ColumnDefinition>
    13. <ColumnDefinition Width="0.3*">ColumnDefinition>
    14. <ColumnDefinition>ColumnDefinition>
    15. Grid.ColumnDefinitions>
    16. <Grid.RowDefinitions>
    17. <RowDefinition>RowDefinition>
    18. <RowDefinition>RowDefinition>
    19. <RowDefinition>RowDefinition>
    20. <RowDefinition>RowDefinition>
    21. Grid.RowDefinitions>
    22. <StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
    23. <TextBlock Text="A1:" VerticalAlignment="Center" >TextBlock>
    24. <TextBox Margin="10" Width="120" Height="35" Text="{Binding LeftNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center">TextBox>
    25. StackPanel>
    26. <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
    27. <TextBlock Text="A2:" VerticalAlignment="Center" >TextBlock>
    28. <TextBox Margin="10" Width="120" Height="35" Text="{Binding RightNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center">TextBox>
    29. StackPanel>
    30. <TextBlock Grid.Row="1" Grid.Column="2" Text="=" VerticalAlignment="Center" HorizontalAlignment="Center">TextBlock>
    31. <StackPanel Grid.Row="1" Grid.Column="3" Orientation="Horizontal">
    32. <TextBlock Text="A3:" VerticalAlignment="Center" >TextBlock>
    33. <TextBox Margin="10" Width="120" Height="35" Text="{Binding ResultNumber, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center">TextBox>
    34. StackPanel>
    35. <StackPanel Grid.Row="2" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Center">
    36. <Button Content="+" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="+">Button>
    37. <Button Content="-" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="-">Button>
    38. <Button Content="*" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="*">Button>
    39. <Button Content="/" Width="100" Height="35" Margin="10" Command="{Binding OperationCommand}" CommandParameter="/">Button>
    40. StackPanel>
    41. Grid>
    42. Window>

     

    注意,在xaml前端UI代码中,分别对TextBox的Text和Button的Command进行了绑定,已达到数据驱动UI,以及UI响应客户的功能

    在UI的构造函数中,将DataContext数据上下文和ViewModel进行关联,如下所示:

    1. namespace DemoMVVM
    2. {
    3. ///
    4. /// Interaction logic for MainWindow.xaml
    5. ///
    6. public partial class MainWindow : Window
    7. {
    8. private MainWindowViewModel viewModel;
    9. public MainWindow()
    10. {
    11. InitializeComponent();
    12. viewModel = new MainWindowViewModel();
    13. this.DataContext = viewModel;
    14. }
    15. }
    16. }

     

    MVVM实例演示

    通过以上步骤,已经完成了MVVM的简单应用。实例演示如下:

    以上就是深入理解WPF中MVVM的设计思想的全部内容。希望可以抛砖引玉,一起学习,共同进步。

  • 相关阅读:
    鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:Navigator)
    golang: Code of Conduct
    2022年,目前大环境下还适合转行软件测试吗?
    (附源码)spring boot酒店管理平台 毕业设计 201010
    vue3之el-table单选
    GPT润色指令
    如何将系统盘MBR转GPT?无损教程分享!
    数据标注的终点会是众包么?(AI数据标注猿交流社区群欢迎加入)
    [直播自学]-[汇川easy320]搞起来(2)看文档
    Revit如何快速做净高分析?方便、快捷的方法来了
  • 原文地址:https://blog.csdn.net/fengershishe/article/details/132999332