原文地址 http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial
简介
先假设大家对c#已经有一定的了解了,并且很容易接受一些关于WPF的知识。因为下面的知识是通过WPF为例的。
我前段时间开始研究了一下WPF,但是找不到什么有帮助的关于MVVM的教程。因此我希望这篇文章能让你眼前一亮。
当我们刚开始学一门新技术时,我们得益于前人的积累。但是从我个人的观点来看,我看过的那些教程什么的都不能满足我,原因如下:
- 例子都是用XAML写的
- 例子根本不提那些能大大简化你开发过程的特性和关键点
- 那些例子的存在只是为了向你炫耀 WPF/XAML对一些无聊特性的兼容性
- 在例子中有一些变量的名字很容易和语言本身的一些关键词和类混淆,这让我们这些新手们很蛋疼
而我现在写的这玩意却没有这些缺点,它是根据我在谷歌搜到的一篇很火的文章改写而来。这篇文章可能不是100%正确,也可能只是许多最佳实践中的一种,但它确实很好地说明了一些我在这几个月里发现的重点。
好吧我们节奏紧凑一点,先说一个关键点然后看几个例子。P.S. 这个UI比较丑,但那不是重点!另外这篇教程比较长,我已经省略了许多代码,所以请下载源代码来观看例子。
一些你要了解的关键点
1. 你应该用ObservableCollection<>而不是List或者Dictionary去存储数据。顾名思义,你的界面必须能Observe(监视)你的数据集。而恰好这个Collection实现了一些能够让它被很好地监控的接口。
2. 每一个WPF控件(包括Window)都有一个DataContext属性,而每一个Collection控件都有一个ItemsSrouce属性用于绑定数据。
3. INotifyPropertyChanged接口将被广泛地使用在你的界面和后台代码中,它用于传递数据的变更。
Example 1:一个大部分人都这样做的错误做法
来个栗子先! 我们先创建一个Song类,而不是那个二逼的Person类。我们能把Song组织到Album中,甚至更大的集合中。一个简单的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,而图形界面就是我们的View。而ViewModel就是将数据绑定到他们上的魔法师,它把一个简单的Model变成了WPF框架能够使用的东西。我再重申一下,这个类就是我们的Model。
好的现在我们来创建一个SongViewModel。我应该思考的是,我们要展示什么东西出来。假设我们只关心一首歌的演唱者,那这个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中暴露了一个属性,就是想在UI中自动地改变它,反之亦然:
SongViewModel song = ...; // ... enable the databinding ... // change the name song.ArtistName = "Elvis"; // the gui should change
还有一点要说的就是,我们在XAML中这样来创建我们的ViewModel:
<Window x:Class="Example1.MainWindow" xmlns:local="clr-namespace:Example1"> <Window.DataContext> <!-- Declaratively create an instance of our SongViewModel --> <local:SongViewModel /> </Window.DataContext>
这相当于我们在后台代码中这样写:
public partial class MainWindow : Window { SongViewModel _viewModel = new SongViewModel(); public MainWindow() { InitializeComponent(); base.DataContext = _viewModel; } }
当然如果你想用后台代码实现的话你得把XAML中的Window.DataContext去掉:
<Window x:Class="Example1.MainWindow" xmlns:local="clr-namespace:Example1"> <!-- no data context -->
好了,这就是我们的界面:点那个Button没用,因为我们还没绑定数据呢。
数据绑定
还记得我说过要选一个用来展示的属性么?这个属性就是ArtistName,我选这个属性是因为WPF的属性中没有 和它名字相同的属性。(之后是作者的一堆吐槽,不表。)要把ArtistName属性绑定到SongViewModel,我们只要在Xaml中写:
<Label Content="{Binding ArtistName}" />
关键字 “Binding”会把Label的Content和在DataContext中存在的ArtistName的值绑定起来。如你所见,我们把DataContext设为SongViewModel的一个实例,然后就可以在Label中成功地展示_songViewModel.ArtistName了。
但是,你现在点button还是没反应,因为界面接受不到任何关于数据改变的通知。
Example 2:INotifyPropertyChanged接口
我们现在得实现这个INotifyPropertyChanged(简称INPC)接口了。任何实现这个接口的类,都会在值改变的时候发通知给相应监听者。所以我们要改动一下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>
要测试数据绑定了,我们先沿用传统方法:实现button的Onclick函数:
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); } }
这行得通,但我们不该这样写。首先,我们把更新歌手的逻辑放到了界面的后台代码中,但它不应该写在这!这些代码应该只和Window这个界面有关。第二个问题是,如果我们想把onclick中的代码放到别的地方,例如从一个Menu中选择,这就意味着我们得复制粘贴好多次。
这是我们点击按钮后的界面:
Example 3:Commands
在UI中绑定事件有点麻烦。但是WPF提供了一种好方法:ICommand。许多控件都有Command属性,它和Content,ItemsSource的绑定规则一样,除非你需要绑定到一个能够返回一个ICommand的属性。在我们这个碉堡的例子中,我仅仅实现了一个碉堡的类叫RelayCommand,它实现了ICommand接口。
ICommand要求用户定义两个方法:bool CanExecute 和 void Execute。前者告诉用户是否能够执行,它能够用来控制空间是否可用。在我们的例子中,我们不关心这个,所以我们只返回true,表示我们一直能调用Execute成功。
因为我们想重用ICommand的代码,所以就把重复代码放到RelayCommand中。具体的代码可以看压缩包。这是界面:
Example 4:Frameworks现在你可能觉得许多代码都是重复的,实现INPC接口,构造Command。其实这些都是些模版代码,例如我们可以把实现INPC的代码放到一个ObservableObject基类中。而对于RelayCommand类,我们把它放入我们的.Net类库中。这就是你在网上找到的那些MVVM框架做的事(例如Prism Caliburn等等)。
ObservableObject和RelayCommand这两个类是重构之后必然能得到的比较基本的类。所以我把这些类放到一个小的类库中希望能够在以后重用。现在界面是这样:
Example 5: 歌曲的集合,但是这种做法是错的
我之前说过,要想在View(就是你的XAML)中显示数据集,必须把数据放到ObservableCollection中。本例我们创建一个AlbumViewModel类,它把许多个Song聚集起来。我们还构建了一个SongDatabase用于歌曲信息的生成。
这是我们的AlbumViewModel原型:
class AlbumViewModel { #region Members ObservableCollection<Song> _songs = new ObservableCollection<Song>(); #endregion }
你应该会想“这次我的View Model不一样了,我想把songs用一个AlbumViewModel来展示,而不是一个SongViewModel”。
我们还要创建一些ICommand并且把他们绑定到button上。
public ICommand AddAlbumArtist {} public ICommand UpdateAlbumArtists {}
这个例子中,当你点击Add Artist时一切ok,但是点击Update的时候却没反应。你可以去读一下MSDN上的这一页,高亮的黄字说:
为了完全支持数据绑定,你的集合众的每一个属性都应该实现一个合适的变值提醒机制,例如INotifyPropertyChanged接口。
现在我们的界面是这样:
Example 6: 歌曲的集合,正确做法!
在这个最终的例子中,我们修复了AlbumViewModel,让ObservableCollection中包含的变量变成了SongViewModels:
class AlbumViewModel { #region Members ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>(); #endregion // code elided for brevity }
现在我们所有的button都绑定到相应的command上了,后台代码真干净!
最终界面是这样:
结论实例化你的ViewModel
最后一点要说的是当我们在XAML中声明ViewModel时,你没法去传递参数。换句话说,你的ViewModel必须没有,或者说有一个默认的无参构造函数。
当然你也可以在cs代码中实例化ViewModel并且传参给他。
其他框架
还有许多不同的MVVM框架,他们有不同的复杂度,不同的功能,不同的目标技术例如WPF Wp7 Silverlight或三者共有。
最后
希望这6个例子能够让你轻松写出MVVM框架的WPF应用。我已经尝试把我认为的重点都在这篇文章中覆盖了。
引用
- Effective C#
- Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
- C# 4.0 in a Nutshell: The Definitive Reference
- WPF 4 Unleashed
- Josh Smith
高级阅读
- Hello World in MVVM Light in 10 Minutes
- MVVMLight Using Two Views