本篇文章是Barry Lapthorn创作的,感觉写得很好,翻译一下,做个纪念。由于英文水平实在太烂,所以翻译有错或者译得不好的地方请多指正。另外由于原文是针对WPF的,我在原文的基础上做了一些修改,让例子能在silverlight上运行。
原文链接:http://www.codeproject.com/KB/WPF/WpfMvvmQuickStart.aspx
简介
假设你对C++有很好的理解,也对C#有适当的了解,那么准备开始WPF学习将不会太困难。我在六个月前开始着手于WPF,然后可能在谷歌搜索中导致了一个结果,我最后开始明白并且开始使用WPF进行生产(我是否错过了这条船,是另一天的另一个故事了)。
就像学习任何新技术一样,事后你将从中得到益处,在我看来,几乎我所遇到的所有的WPF教程都有以下几个方面的不足:
- 例子全是WPF里面的
- 例子缺少关键的注释,事实上你自己制作一个更容易
- 例子试图炫耀WPF的能力,大量的毫无意义的结果,对你没有任何帮助
- 例子使用的类名中有些属性似乎和框架关键字或类名太相似,因此很难在xaml代码中作为用户定义的标示符来使用(ListBoxGroupStyle的名称很让初学者头疼)。
为了解决这个问题,基于在谷歌上输入“WPF 教程”得到的第一条结果我写下了这篇文章。这篇文章可能不是100%正确,或者甚至是做事情的“唯一正确的方法”,不管怎么样它将阐明一些主要的点,这些点是我在六个月前希望发现的。
我将快速的介绍一些主题,然后展示一个例子来解释或演示每一个观点。因此,我事实上没有试图使GUI更漂亮,因为这不是这篇文章的要点(参见上面的要点)。
因为这个教程相当长,为了简洁我将省略许多代码,因此请下载附加的ZIP文件,然后看里面的例子(.NET4.0/VS2010)。每一个例子都是建立在前一个例子上的。
基本要素
- WPF最给力的就是数据绑定,简单的说,你有一些数据,按照某种特征分类放在一个集合里,然后你想将它显示给用户。你可以将数据“绑定”到xaml代码。
- WPF有两个部分,xmal描述你的GUI布局和效果,这个后台代码是绑定到xaml的。
- 一种最优雅的和最大可能被复用的方式来组织你的代码的方法是使用"MVVM"模式:模型,视图,视图模型。
你需要知道的关键点
- 存储数据你应该使用的集合是ObservableCollection<>。而不是list,也不是dictionary,而是ObservableCollection。“Observable”这个词在这里是为这种情况提供:WPF窗口需要能观察到你的数据集合。这个集合类实现了WPF使用的几个接口。
- 每一个WPF控件(包括“窗口”)都有一个“DataContext”,集合控件都有一个“ItemsSource”属性用于绑定。
- “INotifyPropertyChanged”接口将被广泛的的用于GUI和你的代码之间的通信,当数据有任何改变的时候。
例1:错误的做法
开始的最好方法是从例子开始,我们将从一个song类开始,而不是通常的person类,我们可以将歌曲整理到专辑里面,或者是一个大的集合里,或者按艺术家来整理。一个简单的song类应该会像下面这样:
public class Song
{
#region Members
string _artistName;
string _songTitle;
#endregion
#region Properties
///<summary>
/// 艺术家名称
///</summary>
public string ArtistName
{
get { return _artistName; }
set { _artistName = value; }
}
///<summary>
/// 歌曲标题
///</summary>
public string SongTitle
{
get { return _songTitle; }
set { _songTitle = value; }
}
#endregion
}
在WPF术语中,这个叫“模型”,GUI是“视图”。不可思议的是“视图模型”,通过数据绑定将它们绑在一起,它真的是一个很好的适配器能将模型变成某种WPF框架可以使用的东西。所以只是重复一下,这就是“模型”。
自我们创建Song作为引用类型以后,由于副本在内存上很便宜,我们可以非常容易的创建SongViewMode。我们首先需要考虑的是,什么是我们(潜在的)需要显示的?假如我们只关心歌曲的艺术家名称,而不关心歌曲的标题,那么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里暴露了一个属性,我们显然会想使在代码里改变的歌曲的艺术家名称自动的显示在GUI上,反之亦然:
SongViewModel song = ...;
//...允许数据绑定...
//改变名称
song.ArtistName = "Elvis";
//gui应该发生改变
请注意,在所有的例子里面,我们都创建了视图模型的“声明”,例如,我们在xaml代码里这样做:
<UserControl x:Class="Example1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<!--声明创建一个SongViewModel的实例-->
<local:SongViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
</Grid>
</UserControl>
这等价于在后台代码MainWindo.cs里这样做:
public partial class MainWindow : UserControl
{
SongViewModel _viewModel = new SongViewModel();
public MainWindow()
{
InitializeComponent();
base.DataContext = _viewModel;
}
}
然后移除xaml中的DataContext元素:
<UserControl x:Class="Example1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<!--无数据上下文-->
这是运行结果:
点击更新按钮不会进行任何更新,因为我们没有实现数据绑定。
数据绑定
记得我在一开始说,我会选择一个突出的属性。 在这个例子里,我们想显示艺术家姓名。我选择这个名字是因为它和任何WPF属性都不相同。在网络上有无数的例子,选择一个Person类和他的Name属性(它在多个WPF元素中都存在),也许这些文章的作者只是没有意识到这样对初学者来说特别容易混淆(这些文章的目标读者有足够的好奇心)。
有许多的其它关于数据绑定的文章,因此我不能全部包括他们。我希望这个例子是如此微不足道,以至于你可以看清楚它是怎么回事。
绑定SongViewModel的ArtistName属性,我们可以简单的这么做在MianWindow.xaml中:
<TextBlock Text="{Binding ArtistName}"/>
“绑定”关键字绑定这个控件的Text,在这里这个控件是TextBlock,对“ArtistName”这个属性来说由DataContext返回对象。就像你上面所看到的,我们给DataContext设置了一个SongViewModel实例,因此我们在TextBlock上实际是显示的_songViewModel.ArtistName。
再说一次:点击更新按钮不会进行任何更新,因为我们没有实现数据绑定。GUI不会收到任何的关于属性改变的通知。
例2:INotifyPropertyChanged接口
我们必须实现名称为INotifyPropertyChanged的巧妙接口。就像他说的,任何实现了这个接口的类,当属性发生改变的时候会通知所有监听者,所以我们需要修改SongViewModel类稍微多一点点:
public class SongViewModel : INotifyPropertyChanged
{
#region 构造函数
/// <summary>
/// 构造缺省的SongViewModel实例
/// </summary>
public SongViewModel()
{
_song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
}
#endregion
#region 成员
Song _song;
#endregion
#region 属性
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 成员
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region INotifyPropertyChanged 方法
private void RaisePropertyChanged(string propertyName)
{
//得到一个副本以预防线程问题
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
现在这里有几件事发生了。首先,我们检查了我们是否真的改变了属性:这样对大多数复杂对象来说能稍微提高性能。第二,如果值已经改变,我们向所有监听者注册PropertyChanged事件。
那么现在我们有了一个模型,和一个视图模型。我们只需要在定义视图。只需要修改MainWindow:
<UserControl x:Class="Example2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example2"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<!--声明创建一个SongViewModel的实例-->
<local:SongViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="例2 - 它能工作!" />
<TextBlock Grid.Column="0" Grid.Row="1" Text="艺术家: " />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist" Content="更新艺术家姓名" Click="ButtonUpdateArtist_Click" />
</Grid>
</UserControl>
为了测试数据绑定,我们可以用传统方法,创建一个按钮然后写上它的OnClick事件,所以上面的xaml有一个按钮,和Click事件,给出后台代码:
public partial class MainWindow : UserControl
{
#region 成员
/// <summary>
/// 视图模型
/// </summary>
SongViewModel _viewModel;
int _count = 0;
#endregion
public MainWindow()
{
InitializeComponent();
//已经在xaml代码中声明了视图模型实例
//在这里拿到它的引用,因此我们可以在按钮click事件里使用它
_viewModel = (SongViewModel)base.DataContext;
}
private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
{
++_count;
_viewModel.ArtistName = string.Format("Elvis({0})",_count);
}
}
这样就OK啦,但是我们不该这样使用WPF:首先,我们在后台代码里添加了“更新艺术家”逻辑。它不应该属于那里。窗口类与打开窗口是关联的。第二个问题是,假如我们想移动在按钮click事件中的逻辑,这将很难控制。例如,制作一个菜单项,那就意味着我们将要剪切,粘贴,然后在多个地方编辑。
这是改进后的视图,现在点击那里能工作了:
例 3:命令
绑定到GUI事件是有问题的。WPF给你提供了一个更好的方式,那就是ICommand接口。许多控件都有一个命令属性。这些控件同样服从绑定就像Content和ItemSource,除了你需要绑定它到一个属性上外还要返回一个ICommand接口。对于我们在这里看到的微不足道的例子来说,我们仅仅实现了一个被称为“RelayCommand”很小的类,它实现了ICommand接口:
public class RelayCommand:ICommand
{
#region 成员
readonly Func<Boolean> _canExecute;
readonly Action _execute;
#endregion
#region 构造函数
public RelayCommand(Action execute)
:this(execute,null)
{
}
public RelayCommand(Action execute,Func<Boolean> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand 成员
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute();
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_execute();
}
#endregion
}
ICommand接口需要用户定义两个方法:bool CanExecute方法,和void Execute方法。CanExecute方法实际上仅对用户说,我可以执行这个命令吗?这对管理context是很有用的,你可以执行GUI的动作。在我们的例子里,我们不在意这个,所以我们返回true,意味着框架总是可以调用“Execute”方法。你可能会有一种情况,你有一个命令绑定到了按钮上,它只能在你选择了列表里的某一项后才能执行。你可能会在“CanExecute”方法里实现这个逻辑。
由于我们想重复使用ICommand接口的代码,我们使用RelayCommand类,它包含了所有可以重复使用的代码,那些我们不想继续写的代码。
为了展示ICommand是多么容易被复用,我们给一个按钮和一个菜单项都绑定了更新艺术家命令。
<UserControl x:Class="Example3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example3"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<!--声明创建一个SongViewModel的实例-->
<local:SongViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Content="更新艺术家" Grid.Row="0" Grid.ColumnSpan="2" Width="80"
HorizontalAlignment="Left" Command="{Binding UpdateArtistName}"></Button>
<TextBlock Grid.Column="0" Grid.Row="1" Text="例3 - 使用ICommand接口!" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="艺术家: " />
<TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="3" Name="ButtonUpdateArtist" Content="更新艺术家姓名"
Command="{Binding UpdateArtistName}"/>
</Grid>
</UserControl>
注意,我们不再绑定按钮指定的Click事件,或者菜单指定的Click事件。
public partial class MainWindow : UserControl
{
//注意:现在我们没有使用控件指定的事件,我们不需要引用视图模型
public MainWindow()
{
InitializeComponent();
}
}
点击两个按钮都能工作。
例4:框架
如果你已经仔细的阅读过这篇文章,到目前为止,你可能注意到有许多重复的代码:注册INPC,或创建命令。这是大多数代码的样板文件,对于INPC来说,我们可以将它移到一个基类里面以方便我们能“ObservableObject”。对于RelayCommand类来说,我们可以移动它到我们的.NET类库里面。你在网上找到的所有MVVM框架(Prism,Calibum等)都是这样开始的。
就“ObservableObject”和“RelayCommand”类的联系来讲,他们宁愿变得更基本,进行重构是必然的结果。这些类实际上几乎和Josh Smith写的那些类一模一样,这一点也不让人感到吃惊。
所以我们将这些类移动到一个小型类库,以便于我们可以在将来复用。
这结果看起来和以前非常相似:
例5:歌曲集合,错误的做法
我曾经说过,为了在你的试图(例如xaml)里显示集合里的条目,你需要使用ObservableCollection.在这个例子里,我们创建了一个AlbumViewModel,它非常漂亮的把我们的歌曲收集到了一起在人们能理解的一些事情上.我们也引进一个简单的歌曲数据库,只是为了让我们能在这个例子中快速的产生一些歌曲信息.
你的第一次尝试可能会像下面这样:
public class AlbumViewModel
{
#region 成员
ObservableCollection<Song> _songs = new ObservableCollection<Song>();
#endregion
}
你可能会想:"这时候我有一个不同的试图模型,我想作为一个AlbumViewModel显示这些歌曲,而不是SongViewModel".
我们也创建一些更多的命令然后把他们附加到一些按钮上:
public ICommand AddAlbumArtist {}
public ICommand UpdateAlbumArtists {}
在这个例子中,点击"添加艺术家"会工作得很好,不过点击"更新艺术家名称"将不能工作.如果你读了MSDN这一页黄色高亮的注释,它是这样解释的:
为了充分的支持数据值从绑定的数据源对象传递到绑定目标,你的集合中的每一个对象,它支持可绑定的属性必须实现一个恰当的属性改变通知机制,例如INotifyPropertyChanged接口.
我们的视图看起来是这样的:
例6:歌曲集合,正确的做法
在最后这个例子中,我们把AlbumViewModel安装到有一个ObservableCollection的SongViewModels上,以便我们更容易的创建它:
现在我们所有的按钮都绑定到命令上来操作我们的集合,我们的后台代码MainWindow.cs仍然是十分的干净.
我们的视图看起来是这样:
结论
实例化你的视图模型
最后值得一提的是,当你在xaml中声明你的视图模型的时候,你不能给它传递任何参数:换句话说,你的视图模型必须有一个
隐式的或者显示的缺省构造函数.在视图模型上添加多少个状态完全取决于你,你也许发现在Mainwindow.cs后台代码里声明视图模型会更容易,在这里你可以传递构造参数.
其他的框架
有许多其他的复杂度和功能不同的MVVM框架,他们以WPF,WinPho7,Silverlight或者这三个的任何组合作为目标.
最后..
希望这6个例子能向你展示出使用MVVM写一个WPF应用程序是多么的简单,我试图覆盖所有的要点,这些要点是我认为很重要的而且经常在许多文章里被讨论.
如果你发现这篇文章有用,请投一票.
如果你在这篇文章中发现了错误,或者我说的有不对的对方,或者你有一些关于这篇文章的其他问题,请在下面留下评论解释为什么,以及你是怎么样适应他的.
引用
写这篇文章的时候我已经遵守了各种.NET编程指南约定和风格.我在编写这篇文章的时候用到的引用列表列在这里:
- 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