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

MVVM 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (68投票s)

2016 年 7 月 18 日

CPOL

7分钟阅读

viewsIcon

193643

downloadIcon

4631

为绝对初学者介绍 MVVM。

引言

本文旨在帮助初学者从完全不懂 MVVM 到掌握 MVVM。

注意:本文假设您对 XAML 和使用 XAML 的 UI 库(如 WPF)有最基本的了解。只需能读懂即可。为什么?因为在 XAML 出现之前(除非做大量额外工作),MVVM 是不可能实现的,如下文将简要说明。

注意(2):代码示例使用了一些 C# 6 功能(`null` 条件运算符 `a?.b`、自动属性初始化器)和一些 .NET 4.5 功能(`CallerMemberAttribute`),因此至少需要 VS2015 才能编译。不过,它是免费的

背景

如今,关于 MVVM 的文章很多。但不幸的是,它们往往冗长而复杂。本文力求成为您能找到的最简单的 MVVM 文章,同时也会解释您为何会想尝试它。

那么,什么是 MVVM?MVVM 是一种编写 UI 的技术,可以描述为:

简短解释 1

M 代表 Model(模型),这是您的数据。在 MVVM 出现之前,人们一直在使用它。V 代表 View(视图),这是我们将要使其更易于编写的部分。VM 代表 `ViewModel`(视图模型),即 View 的 Model,换句话说,是为了让 MVVM 生效而编写的模型。此外,它可能包含视图特定的属性,例如“选中的项”。

简短解释 2

它是一种代码技术,通过这种技术,您可以像用户一样描述 UI:这里有一个按钮,它执行这个操作;那里有一个文本框,用于输入用户名……您可以大致按照这种方式编写 UI……然后一切都会神奇地工作!

那么,为什么很多 MVVM 文章都那么长呢?那是因为 MVVM 并不能“直接就能用”。您需要将其与支持它的 UI 库一起使用。WPF+XAML 是第一个这样的框架。即便如此,随着人们对 MVVM 的熟悉,仍然有很多东西并非完全奏效。大多数关于 MVVM 的文章要么是大型业务样本,要么是关于填补缺失的部分。本文不会这样做。它将使用开箱即用的工具展示一个非常基础的业务应用程序。但这正是 MVVM 在 WPF 中诞生的原因。

最后,MVVM 适用于开发“数据视图”,例如“`UserView`”或“`SchoolList`”,但不适合底层控件,如 `DatePicker` 或 `TextBox`。

MVVM 的词源

在 CP 论坛上经过一番讨论后,我想添加这一词源部分。虽然它对您理解和实现 MVVM 作为 GUI 编码技术没有任何影响,但它可能会缓解您在讨论该主题时的一些分歧。

MVVM 仅仅是一种 GUI 编码技术,有助于使 GUI 编码更简单、更高效。但它的诞生源于企业开发模式。在这种环境中,拥有多个层是常见的。例如,数据交换层、业务层等。

当 MVVM 诞生时,它拥有许多 UI 接口(最显著的是 `INotifyPropertyChanged`、`INotifyCollectionChanged`、`ICommand`),很明显需要一个新的层。这就是 `ViewModel` 层,它实现了这些接口。

MVVM 中的M是所有那些与 MVVM 作为 GUI 编码技术完全无关的企业层的一个总称。它只是为了与VM,即 `ViewModel`(这对 MVVM 和本文至关重要)形成对比。

开始吧

启动 Visual Studio(VS2015 可免费下载),然后从菜单 **文件 > 新建 > 项目 > Windows > WPF 应用程序** 中创建一个新项目。

打开 `MainWidow.xaml` 文件,并将 `` 标签替换为:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="5"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <DockPanel>
            <TextBlock Text="Added Names" 
            DockPanel.Dock="Top" Margin="5,3"/>
            <ListBox></ListBox>
        </DockPanel>

        <GridSplitter Grid.Column="1" 
        VerticalAlignment="Stretch" Width="5" 
         Background="Gray" HorizontalAlignment="Left" />

        <Grid Grid.Column="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <TextBlock Grid.Row="0" Text="Name" Margin="5,3"/>
            <TextBox Grid.Row="0" Grid.Column="1" Margin="5,3"/>

            <TextBlock Grid.Row="1" Text="Your name is:" Margin="5,3"/>
            <TextBlock Grid.Row="1" Grid.Column="1" Margin="5,3"/>
            
            <Button Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Left" 
             Content="Add Me" Margin="5,3" MinWidth="75" />

        </Grid>

    </Grid>

如果运行应用程序(按 F5),您应该会看到以下内容:

当您单击按钮或编辑文本框时,没有任何反应!让我们来修复这个问题。

第一个 ViewModel

让我们编写一个模型(实际上是一个 Model - View Model 的组合,这样做是可以的),以恰当地表达该表单的意图。

    public class Model
    {
        public string CurrentName { get; set; }
        public List<string> AddedNames { get; } = new List<string>()
    }

现在,我们必须将此模型与视图关联起来。这通过 `DataContext` 属性来完成。

`DataContext` 是一个特殊属性,它将贯穿可视化树的所有元素。将其设置在 `MainWindow` 上,所有控件都可以访问它。让我们将其在窗口构造函数中设置如下:

        public MainWindow()
        {
            InitializeComponent();
            DataContext = new Model();
        }

让我们更新文本框和标签以反映 `CurrentName` 模型属性,并更新列表框以反映 `AddedNames` 属性。以下是视图的新 XAML,更改部分以粗体显示:

            <TextBlock Text="Added Names" 
                           DockPanel.Dock="Top" Margin="5,3"/>
            <ListBox ItemsSource="{Binding AddedNames}">

<!--
   .......
-->

            <TextBlock Grid.Row="0" Text="Name" Margin="5,3"/>
            <TextBox Grid.Row="0" Grid.Column="1" 
             Text="{Binding CurrentName, 
             UpdateSourceTrigger=PropertyChanged}" Margin="5,3"/>

            <TextBlock Grid.Row="1" 
            Text="Your name is:" Margin="5,3"/>
            <TextBlock Grid.Row="1" Grid.Column="1" 
            Text="{Binding CurrentName}" Margin="5,3"/>

我们使用了 `Binding` 这个 XAML 标记扩展。 MarkupExtension 是一种通过编程方式提供值的方式,必须用大括号 `{}` 括起来。 Binding 是 MVVM 功能的核心,是同步 UI 和模型的魔法酱。

运行应用程序,然后……什么都没变!

这里的问题是,尽管 `CurrentName` 属性确实在变化,但 UI 并不知道它在变化。因此,它不会更新。

通知更改接口

现在请出 INotifyPropertyChangedINotifyCollectionChanged 接口。`ViewModel` 应该实现这些接口来通知 UI 它们已发生更改。

考虑到这一点,这是我们模型的新代码:

    public class Model : INotifyPropertyChanged
    {
        #region CurrentName

        public string CurrentName
        {
            get { return mCurrentName; }
            set
            {
                if (value == mCurrentName)
                    return;
                mCurrentName = value;
                OnPropertyChanged();
            }
        }
        string mCurrentName;

        #endregion

        public ObservableCollection<string> 
        AddedNames { get; } = new ObservableCollection<string>();


        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged([CallerMemberName]string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

如果现在运行应用程序,当我们更改 `TextBox` 中的值时,标签将实时更新!这两个控件现在通过 Binding“神奇地”同步了。Binding 会将更改传播到 `TextBox` 和模型属性之间,以及从模型属性到 `TextBlock`,只要它因任何原因发生更改。

专业提示:编写一个触发 `PropertyChangedEvent` 的模型属性,即使它只有 8 行长,也可能非常繁琐。使用 代码片段可以减少错误率!

`AddedNames` 现在是一个 ObservableCollection,它是 `IList` 和 `INotifyCollectionChanged` 的开箱即用实现。

ICommand

按钮和其他可操作项(如 `MenuItem`)通过一个名为 ICommand 的接口工作。WPF 一个明显的疏忽是它没有提供一个开箱即用、简单、对模型友好的 `ICommand` 实现。您可以参考 Josh Smith 的经典 RelayCommand……但由于它是一个非常简单的接口,而且我不想使用任何第三方库,所以我们将在代码中直接实现它……将此添加到 `Model` 类中:

        public Model()
        {
            AddCommand = new AddNameCommand(this);
        }

        class AddNameCommand : ICommand
        {
            Model parent;

            public AddNameCommand(Model parent)
            {
                this.parent = parent;
                parent.PropertyChanged += delegate { CanExecuteChanged?.Invoke(this, EventArgs.Empty); };
            }

            public event EventHandler CanExecuteChanged;

            public bool CanExecute(object parameter) { return !string.IsNullOrEmpty(parent.CurrentName); }

            public void Execute(object parameter)
            {
                parent.AddedNames.Add(parent.CurrentName); ;
                parent.CurrentName = null;
            }
        }

        public ICommand AddCommand { get; private set; }

Needless to say, this `ICommand` could be generalized... I left it as an exercise to the reader as they say! (译:不用说,这个 `ICommand` 可以被通用化……正如他们常说的,我将其留给读者作为练习!)

现在,通过这个简单的 XAML 更改(粗体),让按钮知道命令的存在:

            <Button Grid.Row="2" Grid.ColumnSpan="2" 
            HorizontalAlignment="Left" Content="Add Me" 
                    Command="{Binding AddCommand}"/>

运行应用程序,测试……

太棒了……“添加”按钮现在会根据文本框是否为空自动启用/禁用,将名称添加到列表中,并重置文本框!

恭喜,您现在已经编写了一个 MVVM 应用程序!:)

大部分过程式代码都在模型中。UI 仅仅是声明式的 XAML,通过 Bindings 与模型同步。这就是 MVVM 的本质。

高级主题:DataTemplate

在下面的特定部分,我将不发布完整的代码,建议您下载文章代码并在那里查看代码。.

DataTemplate 是 XAML 片段,可以被其他控件按需用于创建其 UI 的一部分。关于 DataTemplate 和模板(Templates)本身,有很多可以说的。我想在这里简要介绍一下它与 ItemsControl 的关系。

`ListBox` 是一个 `ItemsControl`。有很多 `ItemsControl`(`TreeView`、`MenuItem`、`ListBox` 等)。它们都共享一个功能:显示项目列表。`ItemsControl` 有一个 `ItemsSource` 属性,必须将其设置为一个(可能是可观察的)项目列表,如(上一个示例):

<ListBox ItemsSource="{Binding AddedNames}">

如果列表中的模型稍微复杂一些,例如:
(getter/setter 代码省略,但与上面的 `CurrentName` 相同)。

    public class Person : INotifyPropertyChanged
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public event PropertyChangedEventHandler PropertyChanged;
    }

那么 `ItemsControl`(在此例中是 `ListBox`)将如何知道如何显示 `Person` 对象呢?
通过提供一个 `DataTemplate` 来为每个项目生成视图!对于这些 XAML 片段中的每一个,`DataContext` 将是该项目本身。然后您可以直接绑定到 `Person` 的属性。

        <DataTemplate x:Key="PersonTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding LastName}" FontWeight="Bold" Margin="0,0,5,0"/>
                <TextBlock Text="{Binding FirstName}"/>
            </StackPanel>
        </DataTemplate>

<!--
   ....
-->
            <ListBox ItemsSource="{Binding AddedPersons}"
                     ItemTemplate="{StaticResource PersonTemplate}">
            </ListBox>

给我们

完成了!

您现在已经了解了足够多的知识来开始使用 MVVM,并使您的整体 UI 代码更简单、更动态。祝您编码愉快!:)

历史

这是第一个版本。我不期望有更多更新...

© . All rights reserved.