WPF 的 MVVM 的分析理解(一)

简介

简单的三层架构示例和 GLUE(胶水)代码问题

第一步:最简单的 MVVM 示例 - 把后台代码移到类中

第二步:添加绑定 - 消灭后台代码

第三步:添加执行动作和“INotifyPropertyChanged”接口

第四步:在 ViewModel 中解耦执行动作

第五步:利用 PRISM

WPF MVVM 的视频演示

简介

从我们还是儿童到学习成长为成年人,生命一直都在演变。 对于软件架构, 同样适用这个道理, 从一个基础的架构开始, 随着每个需求和情境在不断演化。

如果你问任何一个 .NET 开发者, 什么是最小的基础架构, 首先浮现的就是"三层架构"。 在这个框架中, 我们把项目分为三个逻辑层次: UI 层, 业务逻辑层和数据访问层, 每一层都负责各自对应的功能。

WPF 的 MVVM 的分析理解(一)_第1张图片

UI 负责显示功能, 业务逻辑层负责校验, 数据访问层负责 SQL 语句。3层架构有如下的好处:

  •  包容变化: 每一层的变化不会重复跨越到其它层次。

  •  重用性: 增强可重用性, 因为每一层都是分离, 自包容的独立实体

MVVM 是三层架构的一个演化。我知道我的经历不够证明这点, 但是我个人对 MVVM 进行了演化和观察。 那我们先从三层基础架构开始, 去理解三层架构存在的问题, 看 MVVM 架构是如何解决这些问题, 然后升级到去创建一个自定义的 MVVM 框架代码。 下面是本文接下来的路线图。

WPF 的 MVVM 的分析理解(一)_第2张图片

简单的三层架构示例和 GLUE(胶水) 代码问题

首先, 让我们来理解三层架构以及它存在的问题, 然后看 MVVM 如何解决这个问题。

直觉和现实是两种不同的事物。 当你看到三层架构的图, 你首先的直觉是每个功能可能都分布在各自层次。 但是当你实际编写代码时, 有些层次被强迫去做一些它们不应该做的额外的工作(破坏了SOLID 原则)。 如果你对 SOLID 原则还不熟悉可以参考这个视频: SOLID principle video(译者注: SOLID 指 Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)。

WPF 的 MVVM 的分析理解(一)_第3张图片

这部分额外工作就在 UI 与Model之间, 以及 Model 与 Data access 之间。 我们把这类代码称为"GLUE"(胶水, 译者注:由于作者全用大写字母表示, 因此后续延用 GLUE)代码。"GLUE"代码主要有两种逻辑类型。

鄙人浅见薄识, 如果你有更多的"GLUE"类型实例, 请在留言中指出。

  • 映射逻辑(绑定逻辑): 每一层通过属性、方法和集合和其它层链接。例如, 一个在 UI 层中名为“txtCustomerName”的 Textbox 控件,将其映射到 customer 类的"CustomerName"属性。

txtCustomerName.text = custobj.CustomerName; // 映射代码

现在谁应该拥有上述绑定逻辑代码,UI 还是 Model?开发者往往把这个代码推到 UI 层次中。

  • 转换逻辑:每个层次使用的数据格式都是不同的。比如一个 Model 类"Person"有一个性别属性,可取值分别为 "F"(Female) 和 "M"(Male) 分别代表女性和男性。但是在 UI 层中,希望将这个值可视化为一个复选框控件,勾选则代表男性,不勾选则代表女性。下面是一个转换代码示例。

if (obj.Gender == “M”) // 转换代码 {chkMale.IsChecked = true;}
else
{chkMale.IsChecked = false;}

大多数开发者最终会将"GLUE"代码写到UI层中。通常可以在后台代码中定位到这类代码,例如 .cs 文件。如果UI 是 XAML,则对应的 XAML.cs 包含 GLUE代码;如果 UI 是 ASPX,则对应的 ASPX.cs 包含 GLUE 代码,以此类推。

那么问题来了:是UI负责这类GLUE代码吗?让我们看下WPF应用中的一个简单的三层结构例子,以及更详细的GLUE代码细节。

下面是一个简单的模型类"Customer",它有三个属性“CustomerName”,“Amount” 和“Married”。

WPF 的 MVVM 的分析理解(一)_第4张图片

但是,当这个模型显示到 UI 上时它又表现如下。所以,你可以看出来它包含了该模型的所有属性,以及一些额外的元素:颜色标签和 Married 复选框控件。

WPF 的 MVVM 的分析理解(一)_第5张图片

下面有一张简单的表,左边是 Model,右边是 UI,中间是谈过的映射和转换逻辑。

你可以看到前两行没有转换逻辑,只有映射逻辑,另外两行则同时包含转换逻辑和映射逻辑。

Model GLUE CODE UI
                Customer Name                 No conversion needed only Mapping                 Customer Name
                Amount                 No conversion needed only Mapping                 Amount
                Amount                 Mapping + Conversion logic.                 > 1500 = BLUE
< 1500 = RED
                Married                 Mapping + Conversion logic.                 True – Married
False - UnMarried

这些转换和映射逻辑代码通常会在“xaml.cs”文件中。下面是上图对应的后台代码,你可以看到映射代码和颜色判定、性别格式转换代码。我在代码中用注释标注出来,这样你可以看到哪些是映射代码,哪些是转换代码。

lblName.Content = o.CustomerName; // mapping code
lblAmount.Content = o.Amount; // mapping code

if (o.Amount > 2000// transformation code
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue);
}
else if (o.Amount > 1500// transformation code
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Red);
}
if (obj.Married == "Married"// transformation code
{
chkMarried.IsChecked = true;
}
else
{
chkMarried.IsChecked = false;
}

现在这些 GLUE 代码存在的问题:

  • 单一责任原则被破坏(SRPViolation): 是 UI 负责这些 GLUE 代码吗?这种情况下改变了 Amount 数量,同时也需要修改 UI 代码。现在,数据的改变为什么会让我去修改 UI 的代码?这里可以闻到坏代码的味道。UI 应该只在我修改样式,颜色和布局的时候才改变。

  • 重用性: 如果我想把同样的颜色逻辑和性别格式转换用到下面的编辑界面,我该怎么做?拷贝粘帖重复的代码?

WPF 的 MVVM 的分析理解(一)_第6张图片

如果我想走得更远一点,把这个 GLUE 代码用在不同的 UI 技术体系上,比如 MVC、Windows Form 或者 Mobile 应用上。

WPF 的 MVVM 的分析理解(一)_第7张图片

但是这里跨 UI 技术平台的重用实际上是不可能的,因为每个平台 UI 背后都和各自的 UI 技术体系耦合得很紧密。

比如,下面的后台代码是继承自“Windows”类,而“Windows”类是集成在 WPF UI 体系中。如果我们想在 Web 应用或者 MVC 中应用这些逻辑,却又无法去创建一个这样的类对象来使用。

public partial class MainWindow : Window
{
// Behind code is here
}

那么我们要怎么重用后台代码?怎么遵循 SRP 原则?

第一步:最简单的 MVVM 示例 - 把后台代码移到类中

我想大部分开发者已经知道怎么解决这个问题。毫无疑问地把后台代码(GLUE 代码)移到一个类库中。这个类库代表了描述了 UI 的属性和行为。任何移入到这个类库的代码都可以编译成 DLL,然后被所有 .NET 项目(Windows,Web 等等)所引用。因此,在这一节我们将创建一个最简单的 MVVM 示例,然后在后续的章节中我们将基于这个示例创建更高级的 MVVM 示例。

WPF 的 MVVM 的分析理解(一)_第8张图片

我们创建一个“CustomerViewModel”类来包含 GLUE 代码。“CustomerViewModel”类代表了你的 UI,所以我们想保持它的属性和UI命名约定一致。你可以从下图看出来“CustomerViewModel”类的属性是如何从之前的 CustomerModel 类中映射过来: “TxtCustomerName”对应“CustomerName”,“TxtAmount”对应“Amount”等等。

WPF 的 MVVM 的分析理解(一)_第9张图片

下面是实际代码:

public class CustomerViewModel 
    {
        private Customer obj = new Customer();

        public string TxtCustomerName
        {
            get { return obj.CustomerName; }
            set { obj.CustomerName = value; }
        }        

        public string TxtAmount
        {
            get { return Convert.ToString(obj.Amount) ; }
            set { obj.Amount = Convert.ToDouble(value); }
        }


        public string LblAmountColor
        {
            get 
            {
                if (obj.Amount > 2000)
                {
                    return "Blue";
                }
                else if (obj.Amount > 1500)
                {
                    return "Red";
                }
                return "Yellow";
            }
        }

        public bool IsMarried
        {
            get
            {
                if (obj.Married == "Married")
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

        }}

关于“CustomerViewModel”这个类有以下几点注意:

  •  类属性都以 UI 的命名方式来约定,这样看上去会更形象一些;

  •  这个类负责了类型转换的代码,使得 UI 看上去更轻量级。例如代码中的“TxtAmount”属性。在 Model 类中的“Amount”属性是数字,而转换的过程是在 ViewModel 类中完成。换句话说这个类负责了 UI 显示的所有职责(译者注:逻辑上的业务职责)让 UI 后台代码看上去更简洁;

  •  所有转换逻辑的代码都在这个类中,例如“LblAmountColor”属性和“IsMarried”属性;

  •  所有的属性数据都保持了简单的字符类型,这样可以在大多 UI 技术平台上适用。例如,“LblAmountColor”属性把颜色值用字符串来传递,这样可以在任何 UI 类型中重用,同时我们也保持了最小的数据共性。

现在“CustomerViewModel”类包含了所有的后台代码逻辑,我们可以创建这个类的对象并绑定到 UI 元素上。你可以在下面代码看到我们只剩下了映射逻辑的代码部分,而转换逻辑的"GLUE"代码已经没有了。

private void DisplayUi(CustomerViewModel o)
{
lblName.Content = o.TxtCustomerName;
lblAmount.Content = o.TxtAmount;
BrushConverter brushconv = new BrushConverter();
lblBuyingHabits.Background = brushconv.ConvertFromString(o.LblAmountColor) as SolidColorBrush;
chkMarried.IsChecked = o.IsMarried;
}

第二步:添加绑定 - 消灭后台代码

第一步的方法很好,但是我们知道后台代码仍然还有问题,在 WPF 中消灭所有后台代码是完全可能的。接下来 WPF 绑定和命令登场了。

WPF 以其绑定(Binding)、命令(Commands)和声明式编程(Declarative programming)而著称。声明式编程意味着你可以使用 XMAL 来表达你的 C# 代码,而不用编写完整的C#代码。绑定功能帮助一个 WPF 对象连接到其它的 WPF 对象,从而他们可以发送和接收数据。

当前的映射 C# 代码有三个步骤:

  • 导入:我们要做的第一件事情是导入“CustomerViewModel”名称空间。

  • 创建对象:下一步要创建“CustomerViewModel”类的对象。

  • 绑定代码:最后将 WPF UI 绑定到这个 ViewModel 对象。

下面表格展示了 C# 代码和与其对应相同的 WPF XAML 代码。


C# code XAML code
                Import                 using CustomerViewModel;                 xmlns:custns="clr-
namespace:CustomerViewModel;assembly=Custo
merViewModel"
                Create
object
                CustomerViewModelobj = new
CustomerViewModel();
obj.CustomerName = "Shiv";
obj.Amount = 2000;
obj.Married = "Married";

"custviewobj" 
TxtCustomerName="Shiv" TxtAmount="1000" IsMarried=”true”/>
                Bind                 lblName.Content = o.CustomerName;
"lblName"  Content="{Binding 
TxtCustomerName, 
Source={StaticResourcecustviewobj}}"/>


你不需要写后台的代码,我们可以选中 UI 元素,按 F4,如下图中选择指定绑定。这个步骤会把绑定代码插入到 XAML中。

WPF 的 MVVM 的分析理解(一)_第10张图片

选择“StaticResource”来指定映射,然后在 UI 元素和 ViewModel 对象之间指定绑定路径。

WPF 的 MVVM 的分析理解(一)_第11张图片

这时你查看 XAML.CS 文件,它已经没有任何 GLUE 代码,同样也没有转换和映射代码。唯一的代码就是标准的 WPF UI 初始化代码。

{        

public MVVMWithBindings()

       {InitializeComponent();} 

}

第三步:添加执行动作和“INotifyPropertyChanged”接口

应用程序不仅仅只是有 textboxs 和 labels, 同样还需要执行动作,比如按钮,鼠标事件等。 因此让我们添加一个按钮来看看如何把 MVVM 类应用起来。 我们在同样的 UI 上添加了一个‘Calculate tax’按钮,当用户按下按钮,它将根据“Sales Amount”值计算出税值并显示在界面上。

WPF 的 MVVM 的分析理解(一)_第12张图片

因此为了在 Model 类实现上面的功能,我们添加一个“CalculateTax()”方法。当这个方法被执行,它根据薪水范围计算出税值,并将值保存在“Tax”属性值中。

public class Customer
{ 
....
....
....
....
private double _Tax;
public double Tax
{
get { return _Tax; }
}
        public void CalculateTax()
        {
    if (_Amount > 2000)
            {
                _Tax = 20;
            }
            else if (_Amount > 1000)
            {
                _Tax = 10;
            }
            else
            {
                _Tax = 5;
            }
        }
}

由于 ViewModel 类是 Model 类的一个封装,因此我们需要在 ViewModel 类中创建一个方法来调用 Model 的“CalculateTax”方法。

public class CustomerViewModel 
{
        private Customer obj = new Customer();
....
....
....
....
        public void Calculate()
        {
            obj.CalculateTax();
        }
}

现在,我们想要在 XAML 的视图中调用这个“Calculate”方法,而不是在后台编写。不过你不能直接通过 XAML 调用“Calculate”方法,你需要用 WPF 的 command 类。

我们通过使用绑定属性将数据发送给 ViewModel 类,而发送执行动作给 ViewModel 类则需要使用命令。

WPF 的 MVVM 的分析理解(一)_第13张图片

所有从视图元素产生的动作都发送给 command 类,所以第一步是创建一个 command 类。为了创建自定义的 command 类,我们需要实现"ICommand"接口(如下图)。

"ICommand"接口有两个必须要重载的方法:“CanExecute”和“Execute”。在“Execute”中我们放的是希望动作发生时实际执行的逻辑代码(比如按钮按下,右键按下等)。在“CanExecute”中我们放的是验证逻辑来决定“Execute”代码是否应该执行。

WPF 的 MVVM 的分析理解(一)_第14张图片

public class ButtonCommand : ICommand
{
        public bool CanExecute(object parameter)
        {
      // When to execute
      // Validation logic goes here
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
// What to Execute
      // Execution logic goes here
    }
}

现在所有的动作调用都发送到 command 类,然后被路由到 ViewModel 类。换句话说,command 类需要组合ViewModel 类(译注:command 类需要一个 ViewModel 类的引用)。

Route

下面是简短的代码片段,有四点需要注意:

  1.  ViewModel 对象是作为一个私有的成员对象。

  2. 该 ViewModel 对象将通过构造函数参数的方式传递进来。

  3.  目前为止,我们没有在“CanExecute”中添加验证逻辑,它始终返回 true。

  4.  在“Execute”方法中我们调用了 ViewModel 类的“Calculate”方法。

public class ButtonCommand : ICommand
    {
        private CustomerViewModel obj; // Point 1
        public ButtonCommand(CustomerViewModel _obj) // Point 2
        {
            obj = _obj;
        }
        public bool CanExecute(object parameter)
        {
            return true// Point 3
        }
        public void Execute(object parameter)
        {
            obj.Calculate(); // Point 4
        }
    }

你可能感兴趣的:(WPF 的 MVVM 的分析理解(一))