zoukankan      html  css  js  c++  java
  • MVVM

    问题引入
    1 场景一:团队辛辛苦苦完成了一个项目,抱着激动的心情去给用户做demo,而用户给你的反馈是UI很不满意,要重新修改,否则拒绝验收。大规模修改UI,晴天霹雳!
    2 场景二:产品在一家客户上线运行反应不错,公司准备扩大营销市场,寻求更多的客户,此时,不同客户对UI纷纷提出修改意见,众口难调,但是老总发话,客户是上帝!
    问题出来了,按照传统的开发模式是基于CodeBehind这样的方式,UI总是和业务逻辑紧密耦合在一起, UI修改,无法避免的业务逻辑修改随之而来,这无非就是我们老生常谈的解耦问题,有没有办法做到UI层剥离出逻辑层呢?MVVM模式为你排忧解难。

    一 什么是MVVM模式
    MVVM(Model-View-ViewModel)是专为WPF和SilverLight设计的开发模式, 与之类似的有Asp.net程序对应的MVC模式, WinForm程序对应的MVP, 关于MVC, MVP此处不展开论述,详情参考http://msdn.microsoft.com/zh-cn/library/dd381412(v=vs.98).aspx。 但在MVC和MVP模式中, View层都具有很多代码逻辑, 最简单的例子是在MVC中当界面发生交互时View去调用Controler中的某个方法,所以 并没有真正意义上实现View与ViewModel完全分离。
    WPF真正引人入胜、使之与WinForm泾渭分明的特点就是——“数据驱动界面”,何为“数据驱动界面” , 与传统的“事件驱动见面”相比较,数据编程了核心,UI处于从属地位;数据是底层、是心脏,数据变了作为表层的UI就会跟着变、将数据展现给用户;如果用户修改了UI元素上的值,相当于透过UI元素直接修改了底层的数据;围绕着这个核心,WPF准备了很多概念相当前卫的技术,其中包括为界面准备的XAML、为底层数据准备的Dependency Property & Binding和为消息传递准备的Routed Event & Command。 
    Binding和Command技术的出现,也为MVVM模式成为WPF平台下一个优秀的开发模式奠定了基础。通过Binding,可以绑定一个View的Property到ViewModel, ViewModel 对象被设置为视图的 DataContex,如果属性值在 ViewModel 更改,这些新值自动传播到通过数据绑定的视图,实现在ViewModel里可以不通过编写任何逻辑代码就直接更新View,做到View与ViewModel之间的完全松耦合,关于Binding,想了解更多可参见 Data and WPF: Customize Data Display with Data Binding and WPF "。 同样,如果没有WPF中的Command, MVVM也很难展示出它的强大力量,ViewModel可将Command暴露给View, 使得View可以消费command中对应的逻辑功能,对于不熟悉command的朋友,可以参考这篇文章Advanced WPF: Understanding Routed Events and Commands in WPF。
    下面简要介绍一下MVVM每个模块的主要职责
    1) View主要用于界面呈现,与用户输入设备进行交互,在code-Behind中还可以些一些UI的逻辑的,比如一些丰富的动画效果,或者直接设置某个元素的样式等,此外,设置View层的DataContext为对于的ViewModel层的逻辑也是写在code-Behind中。
    2) ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,命令,方法,事件,属性验证等逻辑,用于逻辑实现,负责View与Model之间的通信。
    3) Model就是我们常说的数据模型,用于数据的构造,数据驱动, 主要提供基础实体的属性以及每个属性的验证逻辑。
    MVVM中各个模块的交互方式如图所示:
     

    二 为什么要使用MVVM模式
    MVVM模式的引入能给我们带来什么优势呢?相信这是大多数学习MVVM的人关心的一个主要问题。
    首先我们应该清楚地认识到,MVVM不是适用于任何的项目开发,一个项目是否要上一套框架取决于项目本身的规模和性质,盲目的使用开发模式可能会引起过度开发,通常情况下,企业级的WPF应用软件建议使用,主要优势下面将展开详细阐述。
    1团队层面 统一了项目团队的思维方式,也改变了开发方式,由于View与ViewModel之间的松耦合关系,我们可以轻易做到开发团队与设计团队的明确分工,开发团队可以专注于创建功能强大的 ViewModel 类,而设计团队能够熟练运用Blend等工具能为程序员输出用户友好的试图View的XAML文件。而且,随着项目的进行,不断会有新的成员加入,一个清晰的项目设计模式,能够很大程度地减少他熟悉项目的所需时间,并能够规范的进行接下来的开发维护工作。
    2 架构层面 项目架构更加稳定,模块之间松散的耦合关系使得模块之间的相互依赖性大大降低,这也就意味着项目的扩展性得到了提高,即使以后需要加一些新的模块,或者实现模块的注入,我们也能做到最小的改动,从而保证项目的稳定。
    3 代码层面MVVM的引入也使得项目本身变得模块清晰化,条理化,有助于我们更好地区分哪些逻辑是属于UI操作,哪些逻辑是业务操作,增强了代码的可读性、可测性。对于ViewModel层,Views和Unit tests是两个不同类型的消费者,应用程序中的主要交互逻辑处于ViewModel层,这样,在完成ViewModel之后,我们完全可以有理由相信,我们可以对ViewModel进行单元测试,因为它不依赖于任何UI控件,从这个角度看,似乎UnitTest相比于View而言具备更大的消费能力。

    三 详解ViewModel
     ViewModel是MVVM架构中最重要的部分,负责View与Model直接的通信,对于ViewModel的理解是掌握MVVM的关键,下面我们针对ViewModel进行详细剖析。 
    1 ViewModel的属性ViewModel的属性是View数据的来源,但ViewModel层不能是Model层的简单封装,ViewModel层也不能是View层的简单映射。ViewModel的属性可由三部分组成:一部分是Model的复制属性;另一部分用于控制UI状态。例如Button属性的Disable属性,当操作完成时可以通过这个属性更改通知View做相应的UI变换或者后面提到的事件通知;第三部分是一些方法的参数,可以将这些方法的参数设置成相应的属性绑定到View中的某个控件,然后在执行方法的时候获取这些属性,所以一般方法不含参数。
    2 ViewModel的命令 ViewModel中的命令用于接受View的用户输入,并做相应的处理。我们也可以通过方法实现相同的功能。
    3 ViewModel的事件  ViewModel中的事件主要用来通知View做相应的UI变换。它一般在一个处理完成之后触发,随后需要View做出相应的非业务的操作。所以一般ViewModel中的事件的订阅者只是View,除非其他自定义的非View类之间的交互。
    4 View及ViewModel交互模式
    在View与ViewModel模型之间进行双向的联系的主要方式是通过数据绑定。当正确地使用该设计模式后,每一个View除了纯净的XAML和非常少量的后置代码外不会再包含任何东西,彻底地做到了界面展示和业务逻辑的分离,让程序员更加专注于代码的编写。
    ViewModel也能用来容纳View的状态以及执行View需要的任何命令。
    因为WPF内置了Command模式,对于像Button控件之类的UI元素来说都有一个Command的属性,它是WPF所定义的ICommand类型。可以把这些命令放到ViewModel中并以公有属性的形式暴露出来,这样就可以让View对其进行绑定。这极其强大,因为它可以把ModelView中的可执行代码绑定到窗体的Button上。

    四 MVVM实践
    理论知识已经准备充分,现在是检验真理的时刻,以下是使用了Model-View-ViewModel 设计模式的s世上最简单的WPF应用程序例子,简单加法计算器。
    1 代码结构如下图:
     

    2 CaculatorModel类:
    public class CaculatorModel
        {
            public int Num1 { get; set; }
            public int Num2 { get; set; }
            public int Result { get; set; }
    }

    3 ICommand类型的基类DelegateCommand

    using System;
    using System.Windows.Input;
    namespace MVVMDemo.Commands
    {
        public class DelegateCommand:ICommand
        {
            public DelegateCommand(Action<object> executeCommand, Func<object, bool> canExecuteCommand)
            {
                this.executeCommand = executeCommand;
                this.canExecuteCommand = canExecuteCommand;
            }
            // The specific ExecuteCommand aciton will come from the ViewModel, the same as CanExecuteCommand
            private Action<object> executeCommand;

            public Action<object> ExecuteCommand
            {
                get { return executeCommand; }
                set { executeCommand = value; }
            }

            private Func<object, bool> canExecuteCommand;

            public Func<object, bool> CanExecuteCommand
            {
                get { return canExecuteCommand; }
                set { canExecuteCommand = value; }
            }

            public event EventHandler CanExecuteChanged;

            public bool CanExecute(object parameter)
            {
                if (CanExecuteCommand != null)
                {
                    return this.CanExecuteCommand(parameter);
                }
                else
                {
                    return true;
                }
            }

            public void Execute(object parameter)
            {
                if (this.ExecuteCommand != null) this.ExecuteCommand(parameter);
            }

            public void RaiseCanExecuteChanged()
            {
                if (CanExecuteChanged != null)
                {
                    CanExecuteChanged(this, EventArgs.Empty);
                }
            }
        }
    }
    注:ICommand中有两个方法CanExecute和Execute必须实现,这两个方法分别对应着当Command调用时判断是否能执行和具体执行逻辑。

    4 ViewModelBase类

    using System.ComponentModel;

    namespace MVVM.ViewModel
    {
        public class ViewModelBase : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;

            public void RaisePropertyChanged(string propertyName)
            {
                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }
    注:ViewModelBase实现了接口INotifyPropertyChanged, 在该接口中有一个PropertyChanged事件, 当ViewModel中的Property改变时,允许触发PropertyChanged事件,继而重新绑定数据到UI上。
    5 CaculatorViewModel类
    using System.Windows.Input;
    using MVVM.Model;
    using MVVMDemo.Commands;

    namespace MVVM.ViewModel
    {
        public class CaculatorViewModel:ViewModelBase
        {
            #region Fields

            private int num1;
            private int num2;
            private int result;
            private CaculatorModel model;

            #endregion

            #region Properties

            public int Num1
            {
                get 
                {
                    return num1;
                }
                set
                {
                    num1 = value;
                    this.RaisePropertyChanged("Num1");
                }
            }

            public int Num2
            {
                get
                {
                    return num2;
                }
                set
                {
                    num2 = value;
                    this.RaisePropertyChanged("Num2");
                }
            }

            public int Result
            {
                get
                {
                    return result;
                }
                set
                {
                    result = value;
                    this.RaisePropertyChanged("Result");
                }
            }

            #endregion

            #region Commands

            public ICommand CaculateCommand{get;set;}
            public ICommand ClearCommand { get; set; }

            #endregion

            #region Methods

            public void Add(object param)
            {
                Result = Num1 + Num2;
            }

            public void Clear(object param)
            {
                Result = 0;
                Num1 = 0;
                Num2 = 0;
            }

            public void InitilizeModelData()
            {
                // In gernal, the data comes from database
                var model = new CaculatorModel()
                {
                    Num1 = 1,
                    Num2 = 1,
                    Result = 2
                };

                Num1 = model.Num1;
                Num2 = model.Num2;
                Result = model.Result;
            }

            public CaculatorViewModel()
            {
                CaculateCommand = new DelegateCommand(Add, null);
                ClearCommand  = new DelegateCommand(Clear, null);

                InitilizeModelData();
            }

            #endregion
        }
    }
    6 简单计算器的UI

     

    该View所对应的XAML文件如下:
    <Window x:Class="MVVM.View.CaculatorView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="CaculatorView" Height="300" Width="682">
        <Grid Width="596">
            <TextBox Height="23" HorizontalAlignment="Left" Margin="41,90,0,0" Name="txtNum1" VerticalAlignment="Top" Width="120" Text="{Binding Num1}"/>
            <TextBox Height="25" HorizontalAlignment="Left" Margin="195,88,0,0" Name="txtNum2" VerticalAlignment="Top" Width="120" Text="{Binding Num2}"/>
            <Label Content="+" Height="28" HorizontalAlignment="Left" Margin="167,88,0,0" Name="label1" VerticalAlignment="Top" />
            <TextBox Height="25" HorizontalAlignment="Left" Margin="364,88,0,0" Name="textBox5" VerticalAlignment="Top" Width="120"  Text="{Binding Result}"/>
            <Button Content"=" Height="23" HorizontalAlignment="Left" Margin="328,90,0,0" Name="button1" VerticalAlignment="Top" Width="28" Command="{Binding CaculateCommand}" />
            <Button Content="Clear" Height="26" HorizontalAlignment="Left" Margin="501,88,0,0" Name="button2" VerticalAlignment="Top" Width="45" Command="{Binding ClearCommand}" />
        </Grid>
    </Window>
    6. View的Code-Behind
    using System.Windows;
    using MVVM.ViewModel;

    namespace MVVM.View
    {
        /// <summary>
        /// CaculatorView.xaml 的Ì?交?互£¤逻?辑-
        /// </summary>
        public partial class CaculatorView : Window
        {
            public CaculatorView()
            {
                InitializeComponent();
                this.DataContext = new CaculatorViewModel();
            }
        }
    }

  • 相关阅读:
    一则由表单提交引发的思考
    前端技术栈持续汇总中(已解锁)
    5599充值中心功能开发
    CSS动画持续汇总中
    编程小技巧持续汇总中
    开发软件安装方法汇总
    HashMap中tableSizeFor
    2019年JVM面试都问了什么?快看看这22道面试题!(附答案解析)
    Spring注解@EnableWebMvc使用坑点解析
    线程池中 work 为何要实现 AbstractQueuedSynchronizer
  • 原文地址:https://www.cnblogs.com/wangxiaoshuai0401/p/6139879.html
Copyright © 2011-2022 走看看