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

MVVM 模式变得简单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (181投票s)

2011年11月7日

CPOL

18分钟阅读

viewsIcon

801656

downloadIcon

23996

本文概述了 MVVM 模式、其用法和优点

重要提示

朋友们,如果您能在评论区留下只言片语,告诉我您认为这篇文章可以如何改进,以及您希望我涵盖哪些关于 MVVM 的其他主题,我将不胜感激。谢谢。

引言

随着许多公司从 WinfForms 转向 WPF/Silverlight,最近有几位项目经理向我提出了几乎相同关于 MVVM 的问题

  • 什么是 MVVM 模式?
  • MVVM 方法有哪些优点?

本文旨在回答这些问题,并以最简单的方式解释 MVVM 模式。

我假设本文的读者之前没有接触过 MVVM 模式,但对 WPF 或 Silverlight 有一些了解。

如果时间允许,我计划撰写更多关于 MVVM 的文章,其中包括对不同 MVVM 框架的比较,并介绍一个新的 MVVM 框架。

MVVM 概述

Model-View-ViewModel (MVVM) 模式将用户界面代码分为三个概念部分:Model、View 和 ViewModel,其中 ViewModel 的概念是新的,也是最令人兴奋的。

  • Model 是代表来自服务或数据库数据的类集合。
  • View 是对应数据的可视化表示的代码,用户看到并与之交互的方式。
  • ViewModel 作为 View 和 Model 之间的粘合剂。它包装了 Model 中的数据,并使其易于被 View 展示和修改。ViewModel 还控制着 View 与应用程序其余部分(包括任何其他 View)的交互。

ViewModel 代码可以(也应该)引用 Model,但 Model 类(如果与 ViewModel 分开)不应了解 ViewModel。View 应该了解 ViewModel,但 ViewModel 不应该了解 View。在上图中,箭头表示 MVVM 的哪个部分了解哪个部分。

与旧模式中的模型类似,MVVM 模式中的 Model 仅仅是来自服务或数据库的数据。通常,Model 可以构建为 ViewModel 的一部分。因此,在我们的示例中,我们将跳过 Model,主要关注 ViewModel、View 及其之间的交互。

与 MVVM 模式相关的 WPF 概念,View 和 ViewModel 之间的通信

MVVM 模式之所以成为可能,是因为 WPF(以及后来的 Silverlight)引入了以下新概念:

  • WPF 绑定 - 每个绑定连接两个属性(可能在两个不同的对象上),以便其中一个发生变化时,另一个也随之变化。
  • WPF 数据模板,它将非可视化数据(ViewModel)转换为可视化表示(View)。
  • WPF 命令(或 Microsoft Expression Blend SDK 交互行为)用于将事件从 View 传递到 ViewModel。

WPF 绑定、WPF 命令和 MS Expression Blend SDK 交互功能为 View 和 ViewModel 之间的通信提供了必要的管道。还可以采用另一种方法来进行此类通信 - C# 事件可用于在 ViewModel 中发生某些情况时触发 View 中的更改。应尽可能避免此方法,因为它意味着 View 中存在代码隐藏(用于注册事件处理程序)。相反,应使用绑定(可能与触发器结合使用)将 ViewModel 中更改的信息传递给 View。

以下图表描绘了 View 和 ViewModel 之间通信的不同方式

查看此图表时,可能会有人问,为什么绑定和 C# 事件的箭头都指向 View,即使如上所述,ViewModel 并不知道 View。答案是,图表中的箭头从导致更改的 MVVM 部分指向接收更改的部分。绑定是在 View 中设置的,即使 ViewModel 不知道 View,ViewModel 中的更改仍会通过绑定传播到 View。对于 C# 事件,ViewModel 事件的事件处理程序也应在 View 的代码隐藏中设置,以便当 ViewModel 中的更改发生时,View 功能会触发更改。

WPF 绑定

以下是对理解 MVVM 模式所需的 WPF 绑定的简要(绝非全面)概述。有关绑定的更多信息,请尝试例如 数据绑定概述,或 WPF 中的数据绑定 或互联网上提供的许多其他资源。

WPF 绑定连接两个属性(称为绑定目标属性和绑定源属性),以便其中一个发生变化时,另一个也随之变化。绑定的目标属性应始终是 WPF 依赖属性(有关依赖属性的定义,请查看 WPF 教程)。绑定的源属性应为依赖属性或在属性更改时触发 PropertyChanged 事件。

一个简单的绑定示例位于“BindingSample”解决方案下。它在其窗口中显示学生数据(学生名字和学生平均绩点)。

学生数据由 StudentData 类的对象表示。可以看到 StudentData 有两个属性:StudentFirstNameStudentGradePointAverage

public class StudentData : INotifyPropertyChanged
{
    ...

    string _firstName = null;
    public string StudentFirstName
    {
        get
        {
            return _firstName;
        }
        set
        {
            _firstName = value;
            
            OnPropertyChanged("StudentFirstName");
        }
    }

    double _gradePointAverage;
    public double StudentGradePointAverage
    {
        get
        {
            return _gradePointAverage;
        }

        set
        {
            _gradePointAverage = value;
            OnPropertyChanged("StudentGradePointAverage");
        }
    }
}

这些属性充当源属性。更改时,这两个属性都会触发 PropertyChanged 事件,该事件在事件参数中携带相应的属性名称。这允许绑定将其目标属性更新为新值。在此简单示例中,StudentData 扮演 ViewModel 的角色。

View 位于 MainWindow.xaml 文件中。它只是在 Grid 面板中显示学生数据。重要的部分是将 ViewModel 的 StudentData 属性连接到 View 中 TextBox 属性的 WPF 绑定。

<TextBox Text="{Binding Path=StudentFirstName}" Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" /> ... <TextBox Text="{Binding Path=StudentGradePointAverage}" Grid.Row="2" Grid.Column="2" VerticalAlignment="Center" />

TextBox 的 Text 属性充当绑定的目标。

查看上面的 XAML 代码,可能会有人想知道 Text 属性如何知道它应该连接到哪个 StudentData 对象。有几种方法可以在绑定中指定源对象,例如,通过设置绑定的 SourceElementNameRelativeSource 属性。但是,当这些属性未设置时,如我们的示例中,绑定源对象假定由相应可视化元素的 DataContext 属性指定。DataContext 属性有一个特性,即它会从父级向下传播到可视化树的后代,除非显式更改,或者除非它在其路径中遇到 ContentControlItemsControl(稍后解释)。因此,一旦将 DataContext 设置为可视化树的根元素(例如 Window),该树下的绝大多数元素将具有相同的 DataContext。MVVM 模式经常使用此方法,方法是将 View 的 DataContext 属性设置为包含该 View 的 ViewModel。

在我们的例子中,DataContext MainWindow.cs 文件中的 MainWindow 类构造函数内设置。

this.DataContext = _studentData;

相应地,MainWindow.xaml 文件中的两个 TextBox 元素将具有与其 MainWindow 父元素相同的 DataContext,并且绑定会将 _studentData 对象中的 StudentFirstNameStudentGradePointAverage 属性连接到相应的 TextBox'es 属性。

当源属性和目标属性相互依赖但又不相等时(它们甚至可以是不同类型),WPF 绑定可以使用 ValueConverters,它将源属性值转换为目标属性值,反之亦然。

DataTemplates

上面的绑定示例展示了如何将 ViewModel 中的属性绑定到 View 中相应的属性。但是,如果我们想展示多个相同结构的data对象,最好将表示代码提取为 DataTemplate

SimpleDataTemplateTest 解决方案包含一个 DataTemplate 示例。ViewModel 与上一个示例完全相同 - 由 StudentData 表示,并在 MainWindow 类构造函数中附加到 View 的 DataContext。然而,View 本身由定义在 MainWindow.xaml 文件中 MainWindow 资源内的 DataTemplateStudentView”定义。

<DataTemplate x:Key="StudentView"
              DataType="this:StudentData">
  ...
</DataTemplate>

要使用 DataTemplate 显示 StudentData 对象,将使用 ContentControl 元素。

<ContentControl Content="{Binding}"
        ContentTemplate="{StaticResource StudentView}" />

它的 Content 属性直接绑定到其 DataContext(因为未指定绑定路径),从而将 Content 设置为 StudentData 对象。其 ContentTemplate 属性设置为“StudentView”数据模板。ContentControl 将数据和 DataTemplate “结合”起来,生成数据的可视化表示。

请注意,ContentControl 的可视子项/后代将具有 ContentControl Content 属性作为其 DataContext。另请注意,DataContext 不会自动传播到 ContentControl Content 属性。为了使 Content 设置为 ContentControl's DataContext,必须显式绑定到它。

ContentTemplate 非常适合在 View 中显示单个数据项,但如果有一个数据集合,则可以使用 ItemsControl 类来显示整个集合。有关 ItemsControl 示例,请参阅 ItemsControlDataTemplateTest.sln 解决方案。

在此解决方案中,ViewModel 由 StudentListViewModel 类表示,该类在其 TheStudents 属性中包含一个 StudentData 对象的集合。ItemsControl 元素用于显示学生集合。

<ItemsControl ItemsSource="{Binding TheStudents}" ItemTemplate="{StaticResource StudentView}" />

ItemsControl's ItemsSource 属性绑定到学生集合,ItemTemplate 属性为该集合中的每个元素提供 DataTemplate

请注意,ItemsControl 的子单元格将具有 ItemsSource 集合中相应的元素作为其 DataContext

使用命令从 View 向 ViewModel 传递事件

现在,假设 View 中包含一个按钮。我们希望在按下按钮时,ViewModel 执行某个操作。这可以通过使用 WPF 命令或 Microsoft Expression SDK 交互功能来实现。我更喜欢第 2 种方法,但使用命令更普遍,所以我们先在这里讨论它。

Command 对象派生自 ICommand 接口,该接口有两个重要方法:CanExecute 方法控制相应控件(例如 ButtonMenuItem)是否启用或禁用,Execute 方法指定在 ButtonMenuItem 被单击后要执行的操作。在这里,我们使用 DelegateCommand,它允许我们简单地为这两个方法设置任意委托。这些委托在 DelegateCommand 的构造函数中传递。

Command 示例位于 CommandSample.sln 解决方案下。启动解决方案后,按中间的按钮,您将看到一个弹出消息。

CommandSampleViewModel 代表该示例的 ViewModel。它有一个 DelegateCommand 作为其 TheCommand 属性。其“CanExecute”委托始终返回“true”,从而使相应的按钮始终保持启用状态。其“Execute”委托由“OnExecute”函数实现,该函数仅显示一个带有“Command Executed”文本的消息框。请注意,在实际编码中,您不应该在 ViewModel 中放置任何可视化代码,即使是调用 MessageBox.Show。这里只是为了简洁和清晰,以便 OnExecute 函数执行一些用户可以看到的操作。

View 文件 MainWindow.xaml 中的 Button 将其 Command 属性绑定到 ViewModel 的 TheCommand 属性...

<Button Content="Call Command" Width="100" Height="25" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding TheCommand}"/>

...这确保了命令在按钮单击时触发。

使用 Microsoft Expression Blend SDK 交互功能从 View 向 ViewModel 传递事件

WPF 命令有一个缺点,它们只能附加到少数具有 Command 属性的可视控件,例如 ButtonMenuItem,并且它们只在 Click 事件上触发。Microsoft Expression Blend SDK 交互功能在很多方面更为通用,因为它允许为 View 中几乎任何可视化元素上发生的几乎任何事件触发 ViewModel 上的相应方法。

Microsoft Expression SDK 示例可以在 MSExpressionSDKInteractivitySample.sln 解决方案中找到。为了使 Microsoft Expression Blend SDK 交互功能可用于项目,我们需要向项目的引用中添加两个 DLL 文件:Microsoft.Expression.Interactions.dllSystem.Windows.Interactivity.dll。这些文件是 Microsoft Expression Blend SDK 的一部分,但可以独立于 SDK 的其余部分使用。它们位于 MSExpressionBlendSDKDlls 目录中。

该示例演示了如何在 Rectangle 对象上触发 MouseEnter 事件时触发 ViewModel 的方法。启动示例后,将鼠标指针移到中间的蓝色矩形上,您将看到一个弹出消息框。

ViewModel 由 MSExpressionBlendSDKSampleViewModel 类表示。它只有一个方法 MethodToCallOnMouseEnterEvent,该方法会弹出一个消息框(请记住,在实际的 ViewModel 中不应放置任何可视化代码,包括 MessageBox 相关的代码。我在这里放入它只是为了清晰起见)。View 通过 Microsoft Expression Blend SDK 的 EventTriggerCallMethodAction 调用此方法。

 
<Rectangle x:Name="TheRectangle" 
           Fill="Blue" 
           Width="100" 
           Height="100" 
           VerticalAlignment="Center" 
           HorizontalAlignment="Center"> 
    <i:Interaction.Triggers> 
        <i:EventTrigger EventName="MouseEnter" 
                        SourceObject="{Binding ElementName=TheRectangle}"> 
            <se:CallMethodAction MethodName="MethodToCallOnMouseEnterEvent" 
                                 TargetObject="{Binding Path=DataContext, ElementName=TheRectangle}"/> 
        </i:EventTrigger> 
    </i:Interaction.Triggers> 
</Rectangle>

 

与命令不同,Microsoft Expression Blend SDK 功能不提供一种方法来禁用它们“监听”事件的可视元素。这是完全有道理的,因为它们可以与无法禁用的可视化元素一起使用,例如上面示例中的 Rectangle。如果您想提供一种通过 ViewModel 控制元素是否启用的能力,您可以简单地将该元素的 IsEnabled 属性绑定到 ViewModel 中的某个布尔属性。

MVVM 示例

虽然上面的示例展示了如何使用不同的 WPF 组件连接 View 和 ViewModel,但本示例将所有内容整合在一起,并展示了 MVVM 模式的实际应用。

MVVM 示例可以在 MVVMSample.sln 解决方案下找到。它允许从模拟服务器获取学生列表,添加或删除对应于单个学生条目,修改单个学生的属性,并将列表保存到模拟服务器。

整个应用程序的 ViewModel 由 StudentListViewModel 类表示。该类包含 StudentViewModel 类的 ViewModel 对象集合。StudentViewModel 类定义了单个学生的 ViewModel。

当示例启动时,“Save Students”和“Add New Student”按钮被禁用,以强制用户从模拟服务器加载学生数据。一旦加载了学生数据,这些按钮就会启用。这些按钮的启用/禁用状态由 StudentListViewModel 类的 IsSaveStudentsActionEnabledIsAddStudentsActionEnabled 属性控制。当任何这些按钮被单击时需要执行的操作由 ViewModel 的方法 GetStudentsActionSaveStudentsActionAddStudentAction 定义,并通过 Microsoft Expression Blend Interactivity 功能映射到按钮的单击事件。

表示单个学生的 ViewModel 由 StudentViewModel 类的对象定义,该类具有 FirstNameLastNameGradePointAverage 三个属性。它还定义了一个名为 DeleteStudentAction 的方法,用于从其父集合中删除对象。此方法调用 DeleteStudentEvent 事件,该事件触发删除。

相应的 View 定义在位于 XAMLResources 文件夹下的 StudentsViewResources.xaml 资源字典中的 DataTemplatesStudentView StudentViewModel 对象定义的单个学生的 DataTemplate,而 StudentListView 是整个应用程序的 DataTemplate

MainWindow 类仅包含一个 ContentControl,该控件将 StudentListViewModel 对象转换为显示为 StudentListView。

MVVM 讨论

不同的 MVVM 风格

上面提供的所有 MVVM 示例都将 View 表示为 DataTemplates。我认为 DataTemplates 最适合表示 View,因为它们的使用提供了可视化和非可视化功能之间最清晰的分离。然而,在某些项目中,人们更喜欢使用 UserControls 作为 View。根据我的经验,应尽可能避免在 MVVM 模式中使用 UserControls CustomControls,因为它们比模板更重量级,并且包含一些代码隐藏,这些代码隐藏很容易退化为包含一些业务逻辑。

何时使用 UserControls

然而,在构建 UserControl CustomControl 需要的情况下,有两种重要情况:

  • 当您正在构建一个具有全新功能的全新控件时。例如,WPF 不包含内置的图表功能,因此,当您构建一个图表控件时,您将被迫构建一个 UserControl CustomControl ,它接受一些数据输入并基于此绘制图表。
  • 当您被迫使用现有控件的功能,而该现有控件不具备所需的功能,或者不提供绑定到所有数据的方式时,您将不得不扩展该控件以提供所需的功能并将其作为 MVVM View 的构建块。此类控件的一个示例是 DevExpress GridControl,它具有出色的功能和出色的性能,但不是为 MVVM 而设计的,因此需要大量调整。

总之 - 在 MVVM 模式中,不要使用 UserControls CustomControls 来组合 View - 这可以通过 DataTemplates (结合 DataTemplateSelectorsControlTemplatesStyles StyleSelectors)更好地实现。相反,在必要时,使用 User 和 Custom 控件来创建 MVVM 模式中 View 的构建块。

应用程序中的多个 View 和 ViewModel

根据以上讨论,以下是一个构建良好的 MVVM 应用程序中不同模块之间通信的图表。

每个 View 都只与其个人 ViewModel 进行交互。然而,ViewModel 与应用程序的其余功能进行交互:其他 ViewModel、Model、服务等。请注意,ViewModel 仍然需要尽可能独立于彼此,以遵循关注点分离原则。某些 ViewModel 需要相互通信的事实并不意味着它们必须了解彼此的确切类型。它们可以通过通用接口或事件聚合器进行通信(有关事件聚合器的示例,请参阅例如 Prism for Silverlight/MEF in Easy Samples. Part 3 - Communication between the Modules)。

哪个 View 属性应由 ViewModel 控制

潜在地,ViewModel 可以包含任何类型的非可视化对象,包括仅在 View 中使用的对象,例如 Brushes 或文本标签字符串等。例如,我们可以向上面描述的 StudentViewModel 类添加 BackgroundBrush 属性,并将 View 的 Background 属性绑定到它。另一方面,没有理由让 StudentViewModel 包含有关背景颜色的信息。背景画笔可以由 View 很好地指定。将其放入 ViewModel 只会混淆该类的潜在用户。经验法则是,ViewModel 应该只包含 View 中显示的数据以及可以用于与其他 ViewModel 交互的方法和属性。不同的画笔、颜色、动画、颜色和形状的更改,而不影响任何其他 View,都可以由 View 本身通过 WPF 提供的工具(如 Triggers、StyleSelectors DataTemplateSelectors)很好地处理。

MVVM 模式的优点

MVVM 模式的主要优点是它提供了最佳的 关注点分离:在 MVVM 下,View 负责可视化表示,而非可视化的 ViewModel 负责与软件其余部分的所有交互,包括 Model、服务(通常通过 Model)以及其余 ViewModel。所有 MVVM 的优点都源于此特性。

  • 灵活性和可定制性 - 不同的 View 可以与相同的 ViewModel 一起使用,根据不同客户的需求,为相同的功能提供完全不同的可视化表示。
  • 重用性 - 由于可视化和非可视化功能的划分,View 和 ViewModel 都比混合可视化和非可视化功能具有更高的重用潜力,因为例如,非可视化功能通常无法利用包含可视化部分的功能。
  • UI 设计和开发的分离 - MVVM 使得开发人员和设计师的工作能够尽可能清晰地分离:首先,开发人员可以创建一个具有最粗略 GUI 的应用程序。然后,设计师可以使用设计工具修改 GUI(Views),而无需更改任何非可视化代码。
  • 测试 - 为可视化应用程序编写自动化测试并不容易。View-ViewModel 分离允许在没有 View 的情况下对 ViewModel 进行单元测试。由于 ViewModel 负责与应用程序其余部分的交互,因此此类单元测试将涵盖最主要的功能,而 View 测试只需包含对可视化功能的测试,例如颜色、动画等。

MVVM 模式历史

Model-View-ViewModel (MVVM) 模式与 WPF 一起演变,成为设计和构建可视化应用程序的新方法。

它自然地源于 WPF 中开发的新编程概念,如 Bindings 和 DataTemplates。MVVM 模式最初由 Martin Fowler 在 Presentation Model 中提出,由 John Gossman 在 Model-View-ViewModel (MVVM) pattern 中提出。此后,许多其他人也撰写了关于此模式的文章 - 最著名的文章可能是 Josh Smith 在 WPF Apps With The Model-View-ViewModel Design Pattern 中的文章。

结论

本文旨在向对 WPF 或 Silverlight 有一定了解但之前从未接触过 MVVM 的读者介绍 MVVM 模式。我很想听听您关于改进演示清晰度的可能方法的建议。

历史

  • 2011 年 11 月 22 日。修复了源代码中的一些错误。感谢用户 M.Sepahvand 的发现。
© . All rights reserved.