狐狸的小小窝

我是小狐狸~欢迎来我的小窝!
好文自译

WPF/MVVM 入门教程 【翻译 WPF/MVVM Quick Start Tutorial】

这是本人博客新分类“好文自译”的首篇文章。“好文自译”,顾名思义,就是网上看到的自己觉得比较好的文章自己翻译过来以作学习记录之用。当然大部分文章也是很经典很受认可的,网上前辈的翻译也比比皆是。但是看别人的翻译总是觉得不是很满足。虽然自己的水平有限,不见得翻译的就比别人更加高明,但自己翻译一遍必然能加深自己对原文的理解,同时获得那么一些成就感。也请各位看官多多指教了。


本文翻译自Barry Lapthorn大神的MVVM入门文章【WPF/MVVM Quick Start Tutorial】。这篇文章大多数评价都认为讲述的十分清楚易懂。由于本人也是MVVM初学者,故不多作评价,翻译的不妥的地方还请各位看官多多指教。


范例下载 – 74.16KB
本地镜像:

MvvmExample.zip

 

序言

如果你对C#有适当的了解,那么使用WPF不会显得太难。我学习WPF有一段时间了,但是并没有发现什么太好的MVVM教程,所以写了这篇文章,希望能解决这个问题。

无论是学习什么技术,你总能从后见之明中收益。在我看来,基本上我见过的所有WPF教程总是有这么几种问题:

  • 有些范例只有XAML
  • 有些范例会略过那些真正能够让你生活的更容易的要点
  • 有些范例致力于展示WPF/XAML的强大能力,尽管有很多功能对你来说一点用也没有
  • 有些范例使用的类包含了不少属性名字都太像.net框架的关键词和类名了,这导致你很难在(XAML)代码中区分哪些名字是你定义的,哪些是框架的(对新手来说 ListBox GroupStyle 的 Name 属性就是个噩梦)。

本文就是为了解决这些问题。我希望当我在Google上输入“WPF 教程”时第一条结果就是这篇,我也是以此为目标写下的这篇文章。这篇文章可能不是100%准确的,但是它将为你揭示那些我希望在我过去的6个月里能够一次性掌握的要点。

接下来我将会快速的讲述一些要点,并展示一些范例代码来解释这些要点。另外,我并没有给范例做一个美观的界面,毕竟这不是本文的要点。

本教程略长,所以为了简洁起见我省略了相当一部分的代码,完整的代码请下载本文开头附带的ZIP文件。这些代码是用.NET 4.0/VS 2010编写的。

概要

  1. 在WPF中最重要的就是数据绑定。简单来说,你有一些数据,比如按照某种顺序排列的数据集合,你想展示给用户看。你可以把XAML和这些数据进行“绑定”。
  2. WPF由两部分组成:描述GUI布局和效果的XAML,以及与XAML关联的所谓的“后置代码(code-behind)”。
  3. 最简洁并且最可复用的组织代码的方式是使用“MVVM(Model,View,ViewModel)”模式。该模式的目标就是让你的视图基本不要包含任何代码,只有XAML。

你需要知道的关键点

  1. 你需要用ObservableCollection<>来容纳你的数据。不是list,也不能用dictionary,而必须是一个ObservableCollection。所谓的“Observable”是指,WPF窗口需要某种方式去“观察”你的数据集合。这个集合必须实现特定的接口来给WPF使用。
  2. 每个WPF控件(包括window)都声明了一个“DataContext”属性,集合控件则都声明了“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

值得注意的是,在这些范例中,我们都采用了*声明式*的创建我们的视图模型,也就是说我们在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 -->

我们的界面是这样的:
Example1
点击界面中的按钮不会更新任何东西,因为我们还没有完整的实现数据绑定。

数据绑定

还记得我在一开始的时候说过我将选择一个突出的属性名么。在这个例子中,我们将要显示的是ArtistName。我之所以选择这个名字是因为它不是任何WPF属性。在网上有无数的范例都选择Person作为类名并且含有一个叫做Name的属性(很多.net WPF类都含有属性Name)。或许是这些文章的作者都没有考虑到这会给初学者造成多大的困惑(尤其是当这些文章本来就是写给初学者的)。

网上有太多的关于数据绑定的文章了,所以我在这里就不详细讲述了。我希望这些范例足够通俗易懂,足以让你能够明白到底发生了什么。

要将SongViewModel的属性ArtistName绑定到视图上,只需要简单的在MainWindow.xaml这样写:

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

关键词“Binding”将内容和控件绑定在一起。在这个范例中是将DataContext返回内容的属性“ArtustName”绑定在了Label控件上。如上文所述,我们将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 事件。

现在我们有了 ModelViewModel,接着我们需要的就是 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 类只应该关注的是窗口的显示。第二个问题就是,假设我们现在需要增加一个菜单项,其功能和我们的按钮效果一致。那么我们就只好复制、粘贴一遍同样的代码到菜单项的事件里了。

现在我们的界面是这样的,并且按钮点击终于有用了:

Example2

范例3:Command

直接绑定GUI事件的做法问题多多。WPF提供了一个更好的方法:ICommand 接口。许多控件都提供了一个叫做 Command 的属性。它可以像 ContentItemsSource 一样绑定,只是绑定的属性必须返回一个 ICommand 对象。对于我们这样简单的范例来说,我们只是简单的设计了一个叫做“RelayCommand”的类来实现 ICommand 接口。

ICommand 接口有两个方法需要实现:bool CanExecutevoid ExecuteCanExecute 方法只是简单的告诉调用者该命令是否可以执行。当你需要控制控件是否可以响应GUI动作时它很有用处。在我们的范例中我们就直接返回了true,因为我们允许框架在任何时候都可以执行 Execute 方法。但是假如你需要在用户选择了列表中某一项之后才可以点击按钮执行操作的话,那你可以把这写控制逻辑写在 CanExecute 方法中。

出于可复用性角度考虑,我们可以把所有需要重复使用的代码写在 RelayCommand 类中。

接下来我们通过将更新艺术家名称的操作同时绑定给按钮和菜单项来展示 ICommand 是多么容易被复用。注意现在我们已经不需要再去绑定按钮或是菜单项的 Click 事件了。

Example3

范例4:框架

如果你仔细读到这里,你可能会发现我们一直在写一些重复的代码:发送INPC,或者创建command。这些代码简直就是一个模子里刻出来的。对于INPC,我们可以把它移到一个公共基类 “ObservableObject”里,而对于 RelayCommand,我们已经把它添加到了我们的 .NET 类库里了。而这些正是那些风靡网络的MVVM框架们一开始做的事情(比如 Prism、Caliburn等等)。

对于 ObservableObjectRelayCommand 类而言,它们是相当基本的,以至于你只要重构代码必然会最终得到它们。并且毫不意外的是,这些类几乎和 Josh Smith 的代码如出一辙。

于是我们可以把这些类放到一个小的类库中以便我们日后使用。

现在的界面和之前的基本相同:

Example4

 

 

待续

 

4 Comment

  1. 小狐狸,您好:
    我的个人博客刚刚起步,是和互联网有关的,不知能否有幸与您交换友情链接?
    您的博客链接我已经做好了,在我的博客首页左下方。我的博客地址是http://11one.cn ,还请多多指教!

    给您添麻烦了!

  2. 你好,看到你翻譯的這篇文章
    真的受益很多
    網路上太多教學,真的就如同文章內所所的一樣,不夠全面
    甚至還有使用windows form的作法在針對WPF在操作

发表评论

电子邮件地址不会被公开。 必填项已用*标注