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

WPF/MVVM 快速入门教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (695投票s)

2011年3月6日

CPOL

11分钟阅读

viewsIcon

1950669

downloadIcon

61124

WPF 中 MVVM 的快速示例。

引言

假设您对 C# 有相当的了解,那么入门 WPF 并不难。我前段时间开始研究 WPF,但没有找到太多有用的 MVVM 教程。希望这篇文章能解决这个问题。

就像学习任何新技术一样,你会获得事后诸葛亮的优势。从我的角度来看,我遇到的几乎每一个 WPF 教程都因为以下几个原因中的一个或多个而显得不足:

  • 示例完全用 XAML 编写。
  • 示例忽略了那些能真正让你的生活变得更轻松的关键事实。
  • 示例试图通过大量无意义的效果来展示 WPF/XAML 的能力,而这些效果并不能帮助你。
  • 示例使用的类具有与框架关键字和类过于相似的属性,因此在 (XAML) 代码中很难将其识别为用户定义的(`ListBox GroupStyle` 的 `Name` 属性对新手来说是个大麻烦)。

为了解决这个问题,我根据我希望在输入“WPF 教程”后在 Google 上搜索到的第一个结果来编写这篇文章。这篇文章可能不 100% 正确,甚至可能不是“唯一正确的方法”,但它会阐明我在 6 个月前希望在一个地方找到的主要观点。

我将快速介绍一些主题,然后展示一个解释或演示每个要点的示例。因此,我并没有真正尝试让 GUI 变得漂亮,那不是这篇文章的重点(参见上面的要点)。

由于本教程篇幅较长,我已省略了大量代码以求简洁,请下载附件的 zip 文件,并查看示例 (.NET 4.0/VS2010)。 每个示例都建立在前一个示例之上。

基础知识

  1. WPF 中最重要的事情是数据绑定。简而言之,你有一些数据,通常在一个集合中,你想将其显示给用户。你可以将你的 XAML '绑定' 到数据。
  2. WPF 有两部分:描述你的 GUI 布局和效果的 XAML,以及与 XAML 关联的代码隐藏。
  3. 组织代码的最简洁、可能也是最可重用的方法是使用“MVVM”模式:Model、View、ViewModel。它的目标是确保你的 View 包含最少(或没有)的代码,并且应该只包含 XAML。

你需要了解的关键点

  1. 你应该用来保存数据的集合是 `ObservableCollection<>`。不是 `list`,不是 `dictionary`,而是 `ObservableCollection`。这里的“`Observable`”一词是关键:WPF 窗口需要能够“观察”你的数据集合。这个集合类实现了 WPF 使用的某些接口。
  2. 每个 WPF 控件(包括“Window”)都有一个 `DataContext`,而 `Collection` 控件有一个 `ItemsSource` 属性用于绑定。
  3. 接口 `INotifyPropertyChanged` 将被广泛用于在 GUI 和你的代码之间通信任何数据更改。

示例 1:做得(大部分)不对

开始的最佳方式是举个例子。我们将从一个 `Song` 类开始,而不是通常的 `Person` 类。我们可以按专辑、一个大集合或按艺术家来安排歌曲。一个简单的 `Song` 类如下所示:

    public class Song
    {
        #region Members
        string _artistName;
        string _songTitle;
        #endregion

        #region Properties
        /// The artist name.
        public string ArtistName
        {
            get { return _artistName; }
            set { _artistName = value; }
        }

        /// The song title.
        public string SongTitle
        {
            get { return _songTitle; }
            set { _songTitle = value; }
        }
        #endregion
    }

在 WPF 术语中,这是我们的“Model”。GUI 是我们的“View”。将它们绑定在一起的魔力是我们的“ViewModel”,它实际上只是一个适配器,将我们的 Model 转换为 WPF 框架可以使用的东西。所以,再次重申,这是我们的“Model”。

由于我们将 `Song` 创建为引用类型,因此副本成本低廉且内存占用少。我们可以很容易地创建我们的 `SongViewModel`。我们首先需要考虑的是,我们将(可能)显示什么?假设我们只关心 `song` 的艺术家姓名,而不是 `song` 的标题,那么 `SongViewModel` 可以定义如下:

public class SongViewModel
{
    Song _song;

        public Song Song
        {
            get
            {
                return _song;
            }
            set
            {
                _song = value;
            }
        }

        public string ArtistName
        {
            get { return Song.ArtistName; }
            set { Song.ArtistName = value; }
        }
}

但这样并不完全正确。由于我们在 `ViewModel` 中公开了一个属性,我们显然希望代码中对 `song` 艺术家姓名的更改自动显示在 GUI 中,反之亦然。

SongViewModel song = ...;
// ... enable the databinding ...
//  change the name
song.ArtistName = "Elvis";
//  the gui should change

请注意,在所有这些示例中,我们都是“声明式”创建 ViewModel,即我们在 XAML 中这样做:

<Window x:Class="Example1.MainWindow"
        xmlns:local="clr-namespace:Example1">
    <Window.DataContext>
        <!-- Declaratively create an instance of our SongViewModel -->
        <local:SongViewModel />
    </Window.DataContext>

这相当于在你的代码隐藏 `MainWindow.cs` 中这样做:

    public partial class MainWindow : Window
    {
        SongViewModel _viewModel = new SongViewModel();
        public MainWindow()
        {
            InitializeComponent();
            base.DataContext = _viewModel;
        }
    }

并删除 XAML 中的 `DataContext` 元素:

<Window x:Class="Example1.MainWindow"
        xmlns:local="clr-namespace:Example1">
    <!--  no data context -->

这就是我们的 View:

点击按钮不会更新任何内容,因为我们尚未完全实现数据绑定。.

数据绑定

还记得我一开始说我要选择一个突出的属性吗?在这个例子中,我们想显示 `ArtistName`。我选择这个名字是因为它**不**与任何 WPF 属性相同。网上有无数的例子选择一个 `Person` 类,然后是 `Name` 属性(`Name` 属性存在于多个 .NET WPF 类中)。也许文章的作者只是没有意识到这对初学者(而这些人恰恰是这些文章的目标读者)来说特别令人困惑。

网上有几十篇关于数据绑定的文章,所以我在这里就不讲了。我希望这个例子非常简单,让你能看懂。

要绑定到我们 `SongViewModel` 上的 `ArtistName` 属性,我们只需在 `MainWindow.xaml` 中这样做:

  <Label Content="{Binding ArtistName}" />

“`Binding`”关键字将控件(在本例中为 `Label`)的内容绑定到 `DataContext` 返回的对象的“`ArtistName`”属性。如上所示,我们将 `DataContext` 设置为 `SongViewModel` 的一个实例,因此我们实际上是在 `Label` 中显示 `_songViewModel.ArtistName`。

再说一遍:**点击按钮不会更新任何内容,因为我们尚未完全实现数据绑定。GUI 没有收到属性已更改的任何通知。**

示例 2:INotifyPropertyChanged

这就是我们需要实现那个名字很巧妙的接口的地方:`INotifyPropertyChanged`。顾名思义,任何实现此接口的类都会在属性更改时通知任何侦听器。因此,我们需要稍微修改一下我们的 `SongViewModel` 类:

public class SongViewModel : INotifyPropertyChanged
    {
        #region Construction
        /// Constructs the default instance of a SongViewModel
        public SongViewModel()
        {
            _song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
        }
        #endregion

        #region Members
        Song _song;
        #endregion

        #region Properties
        public Song Song
        {
            get
            {
                return _song;
            }
            set
            {
                _song = value;
            }
        }

        public string ArtistName
        {
            get { return Song.ArtistName; }
            set
            {
                if (Song.ArtistName != value)
                {
                    Song.ArtistName = value;
                    RaisePropertyChanged("ArtistName");
                }
            }
        }
        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Methods

        private void RaisePropertyChanged(string propertyName)
        {
            // take a copy to prevent thread issues
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    }

这里现在有几件事情正在发生。首先,我们检查是否真的要更改属性:这对于更复杂的对象来说可以略微提高性能。其次,如果值已更改,我们会向任何侦听器引发 `PropertyChanged` 事件。

现在我们有了一个 `Model` 和一个 `ViewModel`。我们只需要定义我们的 `View`。这只是我们的 `MainWindow`:

<Window x:Class="Example2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Example2"
        Title="Example 2"  SizeToContent="WidthAndHeight" ResizeMode="NoResize"
        Height="350" Width="525">
    <Window.DataContext>
        <!-- Declaratively create an instance of our SongViewModel -->
        <local:SongViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" Grid.Row="0" Content="Example 2 - this works!" />
        <Label Grid.Column="0" Grid.Row="1" Content="Artist:  " />
        <Label Grid.Column="1" Grid.Row="1" Content="{Binding ArtistName}" />
        <Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist"
        Content="Update Artist Name" Click="ButtonUpdateArtist_Click" />
    </Grid>
</Window>

为了测试数据绑定,我们可以采用传统方法,创建一个按钮并将其连接到 `OnClick` 事件,因此上面的 XAML 有一个按钮和 `Click` 事件,给出代码隐藏:

    public partial class MainWindow : Window
    {
        #region Members
        SongViewModel _viewModel;
        int _count = 0;
        #endregion

        public MainWindow()
        {
            InitializeComponent();

            //  We have declared the view model instance declaratively in the xaml.
            //  Get the reference to it here, so we can use it in the button click event.
            _viewModel = (SongViewModel)base.DataContext;
        }

        private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
        {
            ++_count;
            _viewModel.ArtistName = string.Format("Elvis ({0})", _count);
        }
    }    

这还行,但这并不是我们应该使用 WPF 的方式:首先,我们将“更新艺术家”逻辑添加到了我们的代码隐藏中。它不应该在那里。`Window` 类与窗口有关。第二个问题是,假设我们想将 *按钮* 点击事件中的逻辑移动到另一个控件,例如,将其作为一个菜单项。这意味着我们将不得不进行复制粘贴,并在多个地方进行编辑。

这是我们改进后的 View,点击现在可以工作了:

示例 3:Commands

绑定到 GUI 事件存在问题。WPF 为你提供了一种更好的方法。那就是 `ICommand`。许多控件都有一个 `Command` 属性。它们以与 `Content` 和 `ItemsSource` 相同的方式遵循绑定,只是你需要将其绑定到一个返回 `ICommand` 的*属性*。对于我们在这里查看的简单示例,我们只需实现一个名为“`RelayCommand`”的简单类,它实现了 `ICommand`。

`ICommand` 要求用户定义两个方法:`bool CanExecute` 和 `void Execute`。`CanExecute` 方法基本上是告诉用户:我能执行这个命令吗?这对于控制执行 GUI 操作的上下文很有用。在我们的示例中,我们不关心,所以我们返回 `true`,这意味着框架始终可以调用我们的 `Execute` 方法。可能存在一种情况,即你有一个绑定到按钮的命令,并且只有在你从列表中选择了一个项目时才能执行它。你会在 `CanExecute` 方法中实现该逻辑。

由于我们想重用 `ICommand` 代码,我们使用 `RelayCommand` 类,其中包含所有可重复的代码,而我们又不想一直编写。

为了展示重用 `ICommand` 的便捷性,我们将 Update Artist 命令绑定到按钮和菜单项。请注意,我们不再绑定到特定于按钮的 Click 事件或特定于菜单的 Click 事件。

示例 4:框架

到目前为止,如果你仔细阅读,你可能会注意到很多代码都是重复的:引发 INPC 或创建命令。这主要是样板代码,对于 INPC,我们可以将其移到一个我们称之为“`ObservableObject`”的基类中。对于 `RelayCommand` 类,我们将其移入我们的 .NET 类库。这就是你在网上找到的所有 MVVM 框架(Prism、Caliburn 等)的开始之处。

至于 `ObservableObject` 和 `RelayCommand` 类,它们相当基础,是重构的必然结果。毫不奇怪,这些类与 Josh Smith 的类几乎相同。

因此,我们将这些类移到一个我们可以将来重用的小类库中。

View 的外观与之前大致相同:

示例 5:歌曲集合,做得不对

如前所述,为了在你的 `View`(即 XAML)中显示项目集合,你需要使用 `ObservableCollection`。在本例中,我们创建一个 `AlbumViewModel`,它很好地将我们的歌曲收集到一个人们可以理解的东西中。我们还引入了一个简单的歌曲数据库,纯粹是为了快速为本示例生成一些歌曲信息。

你的第一次尝试可能是这样的:

    class AlbumViewModel
    {
        #region Members
        ObservableCollection<Song> _songs = new ObservableCollection<Song>();
        #endregion
    }

你可能会想:“这次我有一个不同的 ViewModel,我想将歌曲显示为 `AlbumViewModel`,而不是 `SongViewModel`。”

我们还创建了更多的 `ICommands` 并将它们附加到一些按钮上:

public ICommand AddAlbumArtist {}

public ICommand UpdateAlbumArtists {}

在此示例中,点击“Add Artist”工作正常。但点击“Update Artist Names”会失败。如果你阅读了此 MSDN 页面上的黄色高亮提示,它解释了原因:

为了完全支持将数据值从绑定源对象传输到绑定目标,你的集合中支持可绑定属性的每个对象都必须实现适当的属性更改通知机制,例如 `INotifyPropertyChanged` 接口。

我们的 View 如下所示:

示例 6:歌曲集合,正确的方式

在最后一个示例中,我们将 `AlbumViewModel` 修改为包含一个 `ObservableCollection` 的 `SongViewModel`,我们之前已经创建了这些 `SongViewModel`:

class AlbumViewModel
{
    #region Members
    ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>();
    #endregion
    //  code elided for brevity
}

现在,所有绑定到命令的按钮都操作我们的集合。**`MainWindow.cs` 中的代码隐藏仍然完全为空。**

我们的 View 如下所示:

结论

实例化你的 ViewModel

最后一点值得一提的是,当你在 XAML 中声明式地声明你的 `ViewModel` 时,你无法向它传递任何参数:换句话说,你的 `ViewModel` 必须有一个隐式或显式的默认构造函数。如何向你的 `ViewModel` 添加状态取决于你自己。你可能会发现,在 `MainWindow.cs` 代码隐藏中声明 `ViewModel` 更容易,在那里你可以传递构造函数参数。

其他框架

还有很多其他 MVVM 框架,它们的复杂性和功能差异很大,针对 WPF、WP7、Silverlight 以及它们的任意组合。

最后...

希望这六个示例向你展示了使用 MVVM 编写 WPF 应用程序的便捷性。我尝试涵盖了我认为重要并且在多篇文章中经常讨论的所有要点。

如果你觉得这篇文章有帮助,请随意投票支持它。

如果你在这篇文章中发现错误,或者我说错了什么,或者你对它有其他问题,请在下方留下评论,解释原因以及你将如何解决它。

参考文献

我在编写本文时,尽量遵守了各种商定的 .NET 编程指南和样式。我在此列出了一些在编写本文时使用的参考资料:

  1. 高效 C#
  2. Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
  3. C# 4.0 in a Nutshell: The Definitive Reference
  4. WPF 4 Unleashed
  5. Josh Smith

延伸阅读(免费链接)

在此基础上,我添加了另外两篇关于使用 Laurent BugnionMVVM Light Framework 的 CP 文章,我建议你开始使用它,以便你第一次掌握 MVVM。如果你理解了这篇文章,请继续阅读:

脚注

(2012/2/2):感谢所有至今为止(以及将来!)投票和/或(积极地!)评论的人!

历史

  • 2011 年 3 月 6 日:初次发布
  • 2012 年 2 月 2 日:补充阅读和脚注
  • 2012 年 2 月 22 日:少量文本编辑以求清晰。

© . All rights reserved.