65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF MVVM 分步教程(基础到高级)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (198投票s)

2014年9月16日

CPOL

17分钟阅读

viewsIcon

882364

本文由 ShivPrasad Koirala 撰写,全面逐步讲解了 WPF MVVM 架构,内容涵盖利用 PRISM、简单的三层架构示例和“胶水代码”问题、添加操作、解耦操作等。


引言

简单的三层架构示例和“胶水代码”问题

级别 1:最简单的 MVVM 示例 – 将后台代码移至类中

级别 2:添加绑定 – 实现零后台代码

级别 3:添加操作和 “INotifyPropertyChanged” 接口

级别 4:从视图模型中解耦操作

级别 5:利用 PRISM

WPF MVVM 视频演示

引言

人生就是一场进化,我们从孩童开始,一路学习,最终成长为成熟的成年人。软件架构也是如此,你从一个基础结构开始,然后根据需求和情况不断演进。

如果你问任何一位 .NET 开发者什么是最小的基础架构,首先想到的就是“三层架构”。在这种架构中,我们将项目分为三个逻辑部分:UI(用户界面)、业务层和数据访问层。每一层都承担并拥有特定的职责。

UI 负责表示层的职责,业务层负责验证,数据访问层负责 SQL 操作。三层架构的优点如下:

  • 变更隔离:- 一层的变更不会扩散到其他层。
  • 可重用性:- 提高了可重用性,因为每一层都是独立的、自包含的个体实体。

MVVM 是三层架构的演进。我知道我没有历史依据来证明这一点,但这是我个人演进和看待 MVVM 的方式。因此,我们将首先从一个基本的三层架构开始,理解三层架构的问题,看看 MVVM 如何解决这个问题,然后逐步创建一个专业的 MVVM 代码。下面是本文其余部分的路线图。

简单的三层架构示例和“胶水代码”问题

所以第一步是理解三层架构、它的问题,然后看 MVVM 如何解决这个问题。

现在,观念和现实是两回事。当你看到三层架构的框图时,你会觉得职责在各层之间得到了合理的分配。但当你真正开始编写代码时,你会发现有些层被迫执行了它们本不应该做的**额外工作**(这违反了 SOLID 原则)。如果你对 SOLID 不熟悉,可以从这个 SOLID 原则视频开始。

这种额外的工作是位于 UI-模型 和 模型-数据访问 之间的代码。我们称之为“胶水(GLUE)”代码。主要有两种逻辑会出现在“胶水”代码中:

我可能看得不够全面,所以如果你有更多的场景,请在下面的评论中提出。

  • 映射逻辑(绑定逻辑):- 每一层都通过属性、方法、集合与其他层连接。例如,UI 层上一个名为 “txtCustomerName” 的文本框会映射到客户类的 “CustomerName” 属性。
txtCustomerName.text = custobj.CustomerName; // mapping code

那么,上述绑定代码逻辑由谁负责,是 UI 还是模型?开发者通常会将这段代码放到 UI 层。

  • 转换逻辑:- 这些层中的数据格式是不同的。例如,一个名为 “Person” 的模型类可能有一个名为 “Gender” 的属性,其值为 “F”(女性)和 “M”(男性),但在 UI 层我们希望将这个值显示为一个复选框,男性显示为“选中”(true),女性显示为“未选中”(false)。下面是一个转换代码的示例。
if (obj.Gender == “M”) // transformation code
{chkMale.IsChecked = true;}
else
{chkMale.IsChecked = false;}

大多数开发者最终会把“胶水”代码写在 UI 层。更具体地说,这些代码通常出现在后台代码中,即 .CS 文件里。所以,如果是 “XAML”,那么 “XAML.CS” 会包含胶水代码;如果是 “ASPX”,那么 “ASPX.CS” 会包含胶水代码,以此类推。

问题是:- 胶水代码是 UI 的职责吗?让我们来看一个 WPF 应用程序的简单三层示例,并更详细地了解胶水代码。

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

但是当这个模型显示在 UI 上时,它看起来如下图所示。你可以看到它包含了模型的所有字段,此外还有一些额外的东西,比如颜色标签和已婚复选框。

下面是一个简单的表格,左边是模型字段,右边是 UI 字段。中间的一列是关于映射和转换逻辑的说明。

你可以看到前两个字段没有任何转换逻辑,只有映射逻辑,而另外两个字段既有映射逻辑也有转换逻辑。

模型 胶水代码 UI
客户名称 无需转换,仅映射 客户名称
金额 无需转换,仅映射 金额
金额 映射 + 转换逻辑。 > 1500 = 蓝色
< 1500 = 红色
已婚 映射 + 转换逻辑。 True – 已婚
False - 未婚

这种转换和映射逻辑通常放在后台代码中,即 “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;
}

现在,关于胶水代码的一些问题:

  • 违反单一职责原则(SRP Violation):- 这些胶水代码是 UI 的职责吗?如果你看当前的情况,金额值的改变也需要修改 UI 代码。那么为什么数据改变了,我还需要修改我的 UI 呢?这里有代码坏味道。UI 应该只在我改变样式、颜色、布局等时才发生变化。
  • 可重用性:- 如果我想在如下所示的编辑界面上使用相同的颜色逻辑和性别转换逻辑,该怎么做?是复制粘贴并创建重复代码吗?

如果我想更进一步,在不同的 UI 技术(如 MVC、Windows Forms 或移动端)中重用这些胶水代码呢?

但是跨 UI 技术的这种可重用性实际上是不可能的,因为 UI 后台代码与 UI 技术紧密耦合。

例如,下面的后台代码继承自 “Window” 类。“Window” 类与 WPF UI 技术紧密相关。所以,如果我们想在 Web 应用程序或 MVC 中使用这个逻辑,我们怎么能创建这个类的对象并使用它呢?

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

那么我们如何重用后台代码,如何遵守单一职责原则呢?

级别 1:最简单的 MVVM 示例 – 将后台代码移至类中

我想大多数开发者已经知道如何解决这个问题了。不用猜也知道,就是把后台代码(胶水代码)移到一个类库中。一个代表 UI 属性和操作的类库。任何代码在移动到类库后,都可以被编译成一个 DLL,并在之后

任何类型的 .NET 项目(Windows、Web 等)中添加引用。因此,在本节中,我们将创建最简单的 MVVM 示例,并在后续章节中将其升级,创建一个专业的 MVVM 示例。

所以让我们创建一个新的类 “CustomerViewModel”,它将包含所有的“胶水”代码。“CustomerViewModel” 类代表你的 UI,所以我们希望类中的属性命名与 UI 的命名规范保持同步。在下图中,你可以看到 “CustomerViewModel” 类如何拥有像 “TxtCustomerName” 这样的属性,它映射到 “CustomerName”;“TxtAmount” 映射到 “Amount”,以此类推。

下面是该类的实际代码。

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 命名规范,如 “TxtCustomerCode”,这样这个类看起来就像是 UI 的一个真实副本。
  • 这个类也负责类型转换代码,从而使 UI 更加轻量级。请看 “TxtAmount” 属性的代码。模型的 “Amount” 属性是数字类型,但类型转换是在视图模型类中完成的。换句话说,这个类承担了 UI 的所有职责,从而使 UI 的后台代码变得可以忽略不计。
  • 所有的转换代码都放在这个类里,请看 “LblAmountColor” 和 “IsMarried” 属性。
  • 所有属性的数据类型都保持为简单的字符串,这样它就可以在各种 UI 技术中被使用。如果你看 “LblAmountColor” 属性,它以“字符串”形式暴露颜色值,这使得该类在任何类型的 UI 中都可重用,因为我们坚持使用最基本的数据类型。

现在,“CustomerViewModel” 类拥有了所有的后台代码逻辑,我们可以创建这个类的对象并将其与 UI 元素绑定。在下面的代码中,你可以看到我们只有映射代码,没有转换逻辑的“胶水”代码。

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;
}

级别 2:添加绑定 – 实现零后台代码

级别 1 的方法很棒,但既然我们知道了后台代码的问题,有没有可能在 WPF 中实现零后台代码呢?这就是 WPF 绑定和命令发挥作用的地方。

WPF 以其强大的绑定、命令和声明式编程而闻名。声明式编程意味着你可以使用 XAML 来表达你的 C# 代码,而不是编写完整的 C# 代码。绑定帮助将一个 WPF 对象与另一个 WPF 对象连接起来,以便它们可以接收/发送数据。

如果你看当前的 C# 映射代码,它有 3 个步骤:

  • 导入:- 我们需要做的第一件事是导入 “CustomerViewModel” 命名空间。
  • 创建对象:- 接下来我们创建 “CustomerViewModel” 类的对象。
  • 绑定代码:- 最后我们将 WPF UI 与视图模型对象绑定。

下表展示了 C# 代码以及对应的 WPF XAML 代码。

  C# 代码 XAML 代码
导入 using CustomerViewModel; xmlns:custns="clr-
namespace:CustomerViewModel;assembly=Custo
merViewModel"
Create
object
CustomerViewModelobj = new
CustomerViewModel();
obj.CustomerName = "Shiv";
obj.Amount = 2000;
obj.Married = "Married";
<Window.Resources>
<custns:CustomerViewModel 
x:Key="custviewobj" 
TxtCustomerName="Shiv" TxtAmount="1000" IsMarried=”true”/>

Bind lblName.Content = o.CustomerName;
<Label x:Name="lblName"  Content="{Binding 
TxtCustomerName, 
Source={StaticResourcecustviewobj}}"/>

你不需要编写绑定代码,我们可以选择 UI 元素,按 F4 并指定绑定,如下图所示。这一步将在 XAML 中插入绑定代码。

要指定映射,你可以选择 “StaticResource”,然后指定视图模型对象和 UI 元素之间的绑定。

如果你查看你的 XAML.CS 的后台代码,会发现它没有任何胶水代码,无论是转换性质还是映射性质的代码都没有。唯一存在的代码是初始化主 WPF UI 的标准 WPF 代码。

public partial class MVVMWithBindings : Window
{
        public MVVMWithBindings()
        {InitializeComponent();}
 }

级别 3:添加操作和 “INotifyPropertyChanged” 接口

应用程序不仅仅是文本框和标签,它还包含按钮、鼠标事件等操作。所以,让我们添加一些像按钮这样的 UI 元素,看看 MVVM 类如何演变。因此,我们在同一个 UI 上添加了一个“计算税款”按钮,当用户按下此按钮时,它会根据“销售金额”计算税款并在屏幕上显示出来。

为了实现上述功能,我们首先从模型类开始,添加一个简单的 “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;
            }
        }
}

由于视图模型是模型类的包装类,我们需要在视图模型中创建一个方法来调用模型的 “Calculate” 方法。

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

现在我们希望通过 XAML 编程从视图中调用这个 “Calculate” 方法,而不是使用后台代码。然而,通过 XAML 你不能直接调用 “Calculate” 方法,你需要使用 WPF 的命令类。

如果你想通过属性向视图模型类发送数据,我们需要使用绑定;如果你想从视图发送操作,我们需要使用命令。

所有来自视图元素的操作都会发送到命令类,所以第一步是创建命令类。要创建一个命令类,我们需要实现 “ICommand” 接口,如下代码所示。

有两个方法必须实现:“CanExecute” 和 “Execute”。在 “Execute” 中,我们放入当操作(按钮点击、右键点击等)发生时我们想要执行的实际逻辑。在 “CanExecute” 中,我们放入验证逻辑,它决定了 “Execute” 代码是否应该运行。

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
    }
}

现在所有的操作调用都首先到达命令类,然后被路由到视图模型类。换句话说,命令类需要持有视图模型类的实例。

下面是简化的代码片段。关于这个代码片段有四个要点需要注意:

  1. 视图模型对象被创建为一个私有的成员级别对象。
  2. 这个对象将由视图模型类通过构造函数传入。
  3. 目前我们还没有在 “CanExecute” 中添加验证逻辑,它总是返回 true。
  4. 在 “Execute” 方法中,我们调用了视图模型类的 “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
        }
    }

在上面的命令代码中,视图模型对象是通过构造函数传递的。所以视图模型类需要创建一个命令对象,并通过 “ICommand” 接口暴露这个命令对象。这个 “ICommand” 接口将在 WPF XAML 中被消费和调用。关于 “CustomerViewModel” 类消费命令类的一些要点:

  1. 命令类是 “CustomerViewModel” 类的一个私有成员级别对象。
  2. 在视图模型类的构造函数中,当前对象实例被传递给命令类。当我们在上一节解释命令类代码时,我们说过命令类的构造函数接收视图模型类的实例。所以,在本节中,我们将当前实例传递给命令类。
  3. 命令对象被暴露为 “ICommand” 接口实例,以便它可以在 XAML 中被使用。
using System.ComponentModel;

public class CustomerViewModel 
{
…
…
private ButtonCommand objCommand; //  Point 1
        public CustomerViewModel()
        {
            objCommand = new ButtonCommand(this); // Point 2
        }
        public ICommand btnClick // Point 3
        {
            get
            {
                return objCommand;
            }
        }
….
….
}

在你的 UI 上添加一个按钮,这样你就可以将按钮的操作连接到暴露的 “ICommand” 方法。现在,转到按钮的属性,滚动到命令属性,右键点击它并选择“创建数据绑定”。

然后选择静态资源并将 “ButtonCommand” 与按钮关联起来。

当你点击“计算税款”按钮时,它会执行 “CalculateTax” 方法并将税款值存储在 “_tax” 变量中。关于 “CalculateTax” 方法的代码,请阅读上一节“级别 3:添加操作和 INotifyPropertyChanged 接口”。

换句话说,UI 不会自动收到税款计算的通知。所以我们需要从对象向 UI 发送某种通知,告诉它税款值已经改变,UI 需要重新加载绑定值。

因此,在视图模型类中,我们需要向视图发送一个 INotify 事件。

要在你的视图模型类中启用通知,我们需要做三件事。这三件事在下面的代码中用注释(如第 1 点、第 2 点和第 3 点)标出。

第 1 点:- 实现 “INotifyPropertyChanged” 接口,如下代码所示。一旦你实现该接口,它会创建一个 “PropertyChangedEventHandler” 事件的对象。

第 2 点和第 3 点:- 在 “Calculate” 方法中,使用 “PropertyChanged” 对象引发事件,并提供该通知针对哪个属性。例如,在这种情况下,它是针对 “Tax” 属性。为了安全起见,我们还会检查 “PropertyChanged” 对象是否不为 null。

public class CustomerViewModel : INotifyPropertyChanged // Point 1
{
….
….
        public void Calculate()
        {
            obj.CalculateTax();
            if (PropertyChanged != null) // Point 2
            {
                PropertyChanged(this,new PropertyChangedEventArgs("Tax"));
            // Point 3
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
}

如果你运行应用程序,你应该能看到点击按钮后 “Tax” 值是如何更新的。

级别 4:从视图模型中解耦操作

到目前为止,我们已经用 MVVM 创建了一个简单的界面,其中实现了属性和命令。我们有一个视图,它的 UI 输入元素(即文本框)通过绑定连接到视图模型,而任何类型的操作(如按钮点击)则通过命令连接。视图模型在内部与模型通信。

但在上述架构中存在一个问题,即命令类与视图模型存在强耦合(HEAVY COUPLING)。如果你还记得命令类的代码(我已将其再次粘贴在下面),我们在构造函数中传递了视图模型对象,这意味着这个命令类无法被其他视图模型类重用。

public class ButtonCommand : ICommand
    {
        private CustomerViewModel obj; // Point 1
        public ButtonCommand(CustomerViewModel _obj) // Point 2
        {
            obj = _obj;
        }
......
......
......

}

但现在让我们逻辑地思考一下,“操作”到底是什么?归根结底,它是来自最终用户的事件,比如鼠标点击(左键或右键)、按钮点击、菜单点击、功能键按下等。所以应该有一种方法来泛化这些操作,并以更通用的方式将其附加到视图模型上。

如果你从逻辑上思考,操作是封装在方法和函数中的逻辑。那么,指向“方法”和“函数”的通用方式是什么……想一想,想一想,想一想……“委托(DELEGATES)”、“委托”和“委托”。

我们需要两个委托,一个用于 “CanExecute”,另一个用于 “Execute”。“CanExecute” 返回一个布尔值,用于验证,并根据验证结果启用或禁用用户界面。“Execute” 将在 “CanExecute” 为 true 时执行。

public class ButtonCommand : ICommand
    {
        public bool CanExecute(object parameter) // Validations
        {
        }
        public void Execute(object parameter) // Executions
        {
        }
    }

换句话说,我们需要两个委托,一个返回布尔值的函数和一个返回 void 的操作。那么创建一个 “Func” 和一个 “Action” 怎么样?“Func” 和 “Action” 这两个都是现成的委托。

如果你对 action 和 func 不熟悉,可以观看这个视频了解一下。

因此,通过使用委托架构,让我们尝试创建一个通用的命令类。我们对命令类做了三处改动(下面是相应的代码),我已将它们标记为第 1、2 和 3 点:-

第 1 点:- 我们从构造函数中移除了视图模型对象,改为接受两个委托,一个是 “Func”,另一个是 “Action”。“Func” 用于验证,即操作何时执行;“Action” 用于执行什么操作。这两个委托值通过构造函数传入,并设置到内部相应的私有委托变量中。

第 2 点和第 3 点:- Func 委托(WhentoExecute)在 “CanExecute” 中被调用,而在 execute 中,Action(Whattoexecute)被调用。

public class ButtonCommand : ICommand
{
private Action WhattoExecute;
private Func<bool> WhentoExecute;
        public ButtonCommand(Action What , Func<bool> When) // Point 1
        {
            WhattoExecute = What;
            WhentoExecute = When;
        }
public bool CanExecute(object parameter)
        {
            return WhentoExecute(); // Point 2
        }
public void Execute(object parameter)
        {
            WhattoExecute(); // Point 3
        }
}

在模型中,我们已经知道要执行什么,即 “CalculateTax”;我们还放了一个简单的函数,命名为 “IsValid”,它将验证 “Customer” 类是否有效。

public class Customer
    {
public void CalculateTax()
        {
if (_Amount > 2000)
            {
                _Tax = 20;
            }
else if (_Amount > 1000)
            {
                _Tax = 10;
            }
else
            {
                _Tax = 5;
            }
        }

public bool IsValid()
        {
if (_Amount == 0)
            {
return false;
            }
else
            {
return true;
            }
        }
    }

在视图模型类中,我们将方法和函数都传递到命令的构造函数中,一个用于 “Func”,一个用于 “Action”。

public class CustomerViewModel : INotifyPropertyChanged
{
private Customer obj = new Customer();
privateButtonCommandobjCommand;
publicCustomerViewModel()
        {
objCommand = new ButtonCommand(obj.CalculateTax,
obj.IsValid);
        }
}

这使得架构更好、更解耦,因为这个命令类可以以一种通用的方式附加到任何视图模型上。下面是改进后的架构,请注意视图模型是如何通过委托(func 和 action)与命令类通信的。

级别 5:利用 PRISM

最后,如果一个框架能为我们的 MVVM 代码减少一些工作量,那就太棒了。PRISM 就是这样一个来救场的框架。PRISM 的主要用途是模块化开发,但它有一个很好的 “DelegateCommand” 类,我们可以用它来代替创建自己的命令类。

所以第一件事是从 http://www.microsoft.com/en-in/download/details.aspx?id=42537 下载 PRISM,编译解决方案并引用两个 DLL:“Microsoft.Practices.Prism.Mvvm.dll” 和 “Microsoft.Practices.Prism.SharedInterfaces.dll”。

现在你可以去掉你的自定义命令类,导入 “Microsoft.Practices.Prism.Commands” 命名空间,并使用委托命令,如下代码所示。

public class CustomerViewModel : INotifyPropertyChanged
{
private Customer obj = new Customer();
private DelegateCommand  objCommand;
public CustomerViewModel()
        {
objCommand = new DelegateCommand(obj.CalculateTax,
                                        obj.IsValid);
        }
…………
…………
…………
…………

}    
}

WPF MVVM 视频演示

我还在下面的 YouTube 视频中从零开始演示了如何用 WPF 实现 MVVM。

如需进一步阅读,请观看以下面试准备和分步教程视频:

访问我的个人资料以获取更多视频。

© . All rights reserved.