WPF学习笔记03-MVVM模型

<0x00> MVVM模型介绍

MVVM模型并不是一个编程的语法,而是一种设计思路
整体分三块(M/V/VM)
M(Model)指数据模型,差不多就是程序的后端部分
V(View)指界面,就是程序前端
VM(View Model),这个不大好翻译,其实就是负责前后端连接

这样做有什么好处
简单说就是低耦合高内聚(网上都这么说的)
换句话说就是修改时更加的灵活,架构更加明白

MVVM模型是从MVC模型发展过来的,主要解决的就是开发过程中代码结构混乱的问题
比方说开发一个项目,分前端后端,如果不采用这些设计思路,很可能两人各搞各的
要连接前后端,既可以是前端控制后端数据,也可以是后端修改前端显示
这样势必会造成整个项目的混乱,不好管理
所以MVVM模型引入了VM作为前后端的中间层
前端的V可以通过VM修改M,后端的M也可以通过VM通知前端的V要界面更新
这样的话,前后端本身就差不多分离了,前后端开发互不干扰
反正最后都是靠写一个VM连接在一起的,当然相应的代码量就多了

大致架构图

总之,MVVM模型不是开发必须的,如果就一个人随便写写,那么肯定是怎么舒服怎么来的
如果是稍微大一点的项目,不采用合适的开发模式,到时候管理代码有的受的

<0x01> 如何在WPF中实现MVVM

WPF里面不是有些.xaml的文件嘛,这些就是我们的V
然后在整个工程中,我们还可以新建很多的.cs文件(这个看你用什么.net的语言)
这些.cs文件就是我们的MVM

当然,为了区分方便,通常会在文件后面跟上后缀
比如*M.cs代表这个文件描述的是一个数据模型
*VM.cs代表这个文件描述的是一个VM
反正怎么习惯怎么来就是

在这篇博客里,我的文件命名没有这么这么规范
因为就像之前说的,MVVM只是一种设计思路

<0x02> 正式开始实现

首先先分析我们的需求,就拿我们之前的计算器吧
前端的实现我们已经有了,就是那个.xaml文件

后端我们就要先分析下了,怎么搞一个计算器的后端
首先我们先思考下一个计算器需要维护什么数据
因为我们打算实现的就是个日常的计算器而不是图形计算器之类的东西
所以肯定要维护一条当前的结果
然后还要一个标志符来保存当前选定的运算
同时还要有个flag来表明正在计算
最后还要维护一个显示的值
所以总共是要维护4条数据

所以对我们的计算器后端文件如下

//Calculator.cs
//常见的引用
namespace WPFTest
{
    public class Calculator
    {
        //这里按道理应该用private,通过对象方法调用这些变量,这里偷下懒
        internal bool flag = false; //表示正在计算
        internal int? calculate = null; //标识当前的运算符
        internal double? ans = null;    //存储当前的结果
        internal string Display = "0";  //当前显示的值

        public void Calculate() //计算方法
        {
            switch (calculate)
            {
                case 1:
                    ans += Double.Parse(Display);
                    break;
                case 2:
                    ans -= Double.Parse(Display);
                    break;
                case 3:
                    ans *= Double.Parse(Display);
                    break;
                case 4:
                    ans /= Double.Parse(Display);
                    break;
            }
        }
    }
}

接下来就是来写我们的VM部分
我这个写得比较繁琐,但就是那个意思

//ViewModel.cs
//常见的引用
namespace WPFTest
{
    //要实现接口INotifyPropertyChanged
    public class ViewModel : INotifyPropertyChanged
    {
        //实现接口的要求,看不懂没关系,这么写就好
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if(handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        //声明一个Calculator对象
        private readonly Calculator _calculator = new Calculator();
        //声明一个Display属性,关键啊
        public string Display
        {
            get
            {
                return _calculator.Display; //定义get,挺正常的写法
            }
            set
            {
                _calculator.Display = value;
                NotifyPropertyChanged("Display");
                //定义set,最后调用的方法通知属性已更改,让前端更新显示
            }
        }

        public void MinusSign_Click()   //减法的实现
        {
            if (_calculator.flag)
            {
                if (Display[0] == '-')
                {
                    Display = Display.Substring(1, Display.Length - 1);
                }
                else
                {
                    Display = "-" + Display;
                }
            }
        }
        //后面方法的实现略
        public ICommand ButtonMinusSign //将减法方法声明成属性
        {
            get
            {
                return new RelayCommand(MinusSign_Click);
                //RelayCommand后面讲
            }
        }
        //后面的方法属性声明略
    }
}

Display要声明成属性的样式,因为xaml里面能绑定的量要是属性
所以后面的按钮方法都要声明成属性,这样才能在xaml里面绑定

当然这里还出现了两个新东西
一个是接口INotifyPropertyChanged,还有RelayCommand
INotifyPropertyChanged字面意思就是通知属性更改
就是说这个类里面有属性在更改时需要通知前端,让前端显示的东西也跟着改
这个接口要求实现一个通知方法,反正就按上面的抄就好了
(大体的原理就是发起一个事件,传回去,让前端知道有东西变了,再回来看)
RelayCommand是自己实现的类,代码如下

//RelayCommand.cs
//常见的声明
namespace WPFTest
{
    //实现ICommand接口
    internal class RelayCommand : ICommand
    {
        //一个只读的Action属性,存储指令
        public Action ExecuteAction { get; }
        public event EventHandler CanExecuteChanged;
        //构造函数
        public RelayCommand(Action executeAction)
        {
            ExecuteAction = executeAction;
        }
        //表示能不能执行(我们这就默认能执行)
        public bool CanExecute(object parameter)
        {
            return true;
        }
        //调用方法
        public void Execute(object parameter)
        {
            ExecuteAction();
        }
    }
}

就是这样,本质就是用泛型委托打包了个方法
这个类也方便我们把方法打包成一个属性,好绑定到xaml

最后终于是我们的前端界面了
其实多的不用改

<Window x:Class="WPFTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFTest"
        d:DataContext="{d:DesignInstance Type=local:ViewModel}"
        mc:Ignorable="d"
        Height="400"
        Width="300"
        WindowStartupLocation="CenterScreen"
        AllowsTransparency="True"
        Background="Transparent"
        WindowStyle="None">
    <!--这里指定了DataContext,让xaml能找到对应的属性-->
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin"
                    Value="6"/>
            <Setter Property="FontSize"
                    Value="24"/>
            <Setter Property="Foreground"
                    Value="White"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border Name="border"
                                Background="#241238"
                                CornerRadius="10">
                            <TextBlock Text="{TemplateBinding Content}"
                                       VerticalAlignment="Center"
                                       HorizontalAlignment="Center"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver"
                                     Value="True">
                                <Setter TargetName="border"
                                        Property="Background"
                                        Value="#190D24"/>
                            </Trigger>
                            <Trigger Property="IsPressed"
                                     Value="True">
                                <Setter TargetName="border"
                                        Property="Background"
                                        Value="Black"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Border CornerRadius="10"
            MouseMove="Border_MouseMove"
            Margin="10">
        <Border.Background>
            <LinearGradientBrush StartPoint="0,0"
                                 EndPoint="0,1">
                <GradientStop Color="#392669"
                              Offset="0"/>
                <GradientStop Color="#46204F"
                              Offset="1"/>
            </LinearGradientBrush>
        </Border.Background>
        <Border.Effect>
            <DropShadowEffect Color="Gray"
                              ShadowDepth="0"
                              BlurRadius="10"
                              Opacity=".5"
                              Direction="0"/>
        </Border.Effect>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0"
                        Orientation="Horizontal"
                        HorizontalAlignment="Right">
                <Button Name="Button_Minimize"
                        Click="Button_Minimize_Click">
                    <Button.Template>
                        <ControlTemplate>
                            <Ellipse Width="16"
                                     Height="16"
                                     Fill="#F0DC4E"/>
                        </ControlTemplate>
                    </Button.Template>
                </Button>
                <Button Name="Button_Close"
                        Click="Button_Close_Click">
                    <Button.Template>
                        <ControlTemplate>
                            <Ellipse Width="16"
                                     Height="16"
                                     Fill="#F0443E"/>
                        </ControlTemplate>
                    </Button.Template>
                </Button>
            </StackPanel>
            <TextBlock Grid.Row="1"
                       Name="Answer"
                       Text="{Binding Display}"
                       FontSize="28"
                       HorizontalAlignment="Right"
                       Margin="20"
                       Foreground="White"/>
            <!--Text这里绑定的是显示的内容-->
            <Grid Grid.Row="2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Button Name="Button_MinusSign"
                        Command="{Binding ButtonMinusSign}"
                        Content="±"
                        Grid.Column="0"
                        Grid.Row="0"/>
                <!--Command这里绑定的就是按钮的指令-->
                <!--后面的Button就不写了-->
            </Grid>
        </Grid>
    </Border>
</Window>

其实就打注释的那三处要变,别的都不用改

<0x03> MVVM模型到底有什么用

说实在的,我刚学完这模型我也是说这玩意有啥用
甚至觉得我是不是少看了些什么
因为从逻辑上,这么写代码,代码量多了,但功能都没变过
就感觉这么写很多余

但实际上,我思考了好一阵(也是这篇博客拖这么久写完的原因)
这个项目太小了,体现不出来
想象一个稍微大点的项目,前后端分别开发的
假设最终就两个主要的文件,一个前端,一个后端
如果说后端突然抽风要改方法名,那前端也得跟着改(这就是耦合的情况)
但如果用MVVM模型,在前后端之间再加层"胶水"层
这样当后端改名的时候,“胶水"层的名字没改,前端就不用改,提高了效率
(这样也叫解耦)

别的好处我也很难讲了,毕竟目前我的开发经验不多
正如我最前面写的,这个只是个思想,并不是什么语法之类的
(总算写完了)

Licensed under CC BY-NC-SA 4.0