WPF/MVVM Quick Start Tutorial
来源:http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial?msg=4486786#xx4486786xx,对关键的地方进行了翻译。
基本点:
1、WPF最重要的事情是数据绑定(data binding)。简而言之,有一些数据(通常情况下是某种排序的集合),你希望能够展示给用户。你可以把数据绑定到xaml上。
2、WPF有两个部分,xaml和后台代码,其中xaml描述了GUI的布局(layout)和效果(effects)。
3、采用'MVVM'模式可以最整洁优雅,最大限度地复用代码,M指的是Model,V指的是View,VM指的是ViewModel。MVVM的目标是保证View包含最少的(或没有)代码,并且View应该仅仅是XAML。
应该知道的关键点:
1、承载数据的集合不应该采用列表或字典,应该采用
ObservableCollection<>
。
Observable的意思是WPF窗体需要能够“观察”数据集合。这个集合类实现WPF能够使用的特定的接口。2、每一个WPF控件(包括窗体)有一个"DataContext",每一个集合控件有一个“ItemsSource”属性(attribute)可以用来绑定。
3、“INotifyPropertyChanged”接口可以用于在GUI和代码之间通信数据的任何变化。
例子一:
一个简单的song类:
Song Model
在WPF技术中,这个是Model。GUI是View。通过ViewModel,数据在Model和View之间绑定。可以这么理解,ViewModel是一个适配器(adapter),将Model转变为WPF框架可以使用的一些东西。
song类是引用类型。创建SongViewModel类,我们需要首先考虑我们准备显示什么?假设,我们仅仅关心音乐的艺术家名称,而不是音乐的名字,那么SongViewModel应该这么定义:
SongViewModel
1 public class SongViewModel 2 { 3 Song _song; 4 5 public Song Song 6 { 7 get 8 { 9 return _song; 10 } 11 set 12 { 13 _song = value; 14 } 15 } 16 17 public string ArtistName 18 { 19 get { return Song.ArtistName; } 20 set { Song.ArtistName = value; } 21 } 22 }
我们希望:
修改艺术家名字
即后台代码变化了,gui也会变化,反之亦然。
注:view的编写方式有两种:
1、XAML:
界面
2、XAML的后台cs:
后台代码
1 public partial class MainWindow : Window 2 { 3 SongViewModel _viewModel = new SongViewModel(); 4 public MainWindow() 5 { 6 InitializeComponent(); 7 base.DataContext = _viewModel; 8 } 9 }
前台
1 <Window x:Class="Example1.MainWindow" 2 xmlns:local="clr-namespace:Example1"> 3 <!-- no data context --> 4 </Window>
运行结果:
期望:点击button的时候,artist的名字发生变化,即Unkonn-->Elvis。
但是并没有变化,因为我们没有完整实现数据绑定。
绑定SongViewModel的ArtistName属性(property),如下(XAML文件中):
前台
1 <Label Content="{Binding ArtistName}" />
“Bingding”绑定了控件(本例中的label)的内容和DataContext返回的对象的“ArtistName”属性(property)。本例,DataContext被赋成了SongViewModel的一个实例(instance),所以实际上GUI显示的是该实例的“ArtistName”属性(property)。
再一次点击button,仍旧没有改变任何事情,因为我们还是没有完全实现数据绑定。GUI没有接收到关于属性已经变化的任何通知。
例子二:
为了解决上面的问题,要采用INotifyPropertyChanged接口。如果一个类实现了这个接口,当属性(property)发生变化的时候,这个类就会通知所有的listeners。修改SongViewModel类如下:
INotifyPropertyChanged
1 public class SongViewModel : INotifyPropertyChanged 2 { 3 #region Construction 4 /// Constructs the default instance of a SongViewModel 5 public SongViewModel() 6 { 7 _song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" }; 8 } 9 #endregion 10 11 #region Members 12 Song _song; 13 #endregion 14 15 #region Properties 16 public Song Song 17 { 18 get 19 { 20 return _song; 21 } 22 set 23 { 24 _song = value; 25 } 26 } 27 28 public string ArtistName 29 { 30 get { return Song.ArtistName; } 31 set 32 { 33 if (Song.ArtistName != value) 34 { 35 Song.ArtistName = value; 36 RaisePropertyChanged("ArtistName"); 37 } 38 } 39 } 40 #endregion 41 42 #region INotifyPropertyChanged Members 43 44 public event PropertyChangedEventHandler PropertyChanged; 45 46 #endregion 47 48 #region Methods 49 50 private void RaisePropertyChanged(string propertyName) 51 { 52 // take a copy to prevent thread issues 53 PropertyChangedEventHandler handler = PropertyChanged; 54 if (handler != null) 55 { 56 handler(this, new PropertyChangedEventArgs(propertyName)); 57 } 58 } 59 #endregion 60 }
现在我们需要验证这个确实可以工作,编写View:
前台
1 <Window x:Class="Example2.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:local="clr-namespace:Example2" 5 Title="Example 2" SizeToContent="WidthAndHeight" ResizeMode="NoResize" 6 Height="350" Width="525"> 7 <Window.DataContext> 8 <!-- Declaratively create an instance of our SongViewModel --> 9 <local:SongViewModel /> 10 </Window.DataContext> 11 <Grid> 12 <Grid.RowDefinitions> 13 <RowDefinition Height="Auto" /> 14 <RowDefinition Height="Auto" /> 15 <RowDefinition Height="Auto" /> 16 </Grid.RowDefinitions> 17 <Grid.ColumnDefinitions> 18 <ColumnDefinition Width="Auto" /> 19 <ColumnDefinition Width="Auto" /> 20 </Grid.ColumnDefinitions> 21 <Label Grid.Column="0" Grid.Row="0" Content="Example 2 - this works!" /> 22 <Label Grid.Column="0" Grid.Row="1" Content="Artist: " /> 23 <Label Grid.Column="1" Grid.Row="1" Content="{Binding ArtistName}" /> 24 <Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist" 25 Content="Update Artist Name" Click="ButtonUpdateArtist_Click" /> 26 </Grid></Window>
code behind
点击按钮,一切正常。
但是这不应该是我们使用WPF的方式:首先,我们在xaml的后台直接添加了“更新艺术家”逻辑,这个逻辑不应该在这里显示。其次,如果我们把在button的点击事件的后台逻辑放到其它控件事件里,则需要在不同的位置剪切赋值和编辑。
例子三:
直接绑定到GUI的事件是有问题的。WFP提供了一种更好的方式。那就是ICommand。许多控件都有命令属性(attribute)。这些属性的绑定方式与Context和ItemsSource相同,只是命令属性需要绑定到一个返回ICommand的属性(property)上。
ICommand需要使用人员定义两个方法:bool CanExecute和void Execute。CanExecute方法实际上仅仅是告诉使用者,我是否能够执行这个命令。比如,button绑定了一个命令,只有一个列表的一项被选中时,这个命令才可以运行,那么你就可以在CanExcute方法里实现逻辑。
本例中的RelayCommand类包含所有重复的代码(即只要用这种命令绑定就可调用的代码),代码如下:
RelayCommand
1 public class RelayCommand : ICommand 2 { 3 #region Members 4 readonly Func<Boolean> _canExecute; 5 readonly Action _execute; 6 #endregion 7 8 #region Constructors 9 public RelayCommand(Action execute) 10 : this(execute, null) 11 { 12 } 13 14 public RelayCommand(Action execute, Func<Boolean> canExecute) 15 { 16 if (execute == null) 17 throw new ArgumentNullException("execute"); 18 _execute = execute; 19 _canExecute = canExecute; 20 } 21 #endregion 22 23 #region ICommand Members 24 public event EventHandler CanExecuteChanged 25 { 26 add 27 { 28 29 if (_canExecute != null) 30 CommandManager.RequerySuggested += value; 31 } 32 remove 33 { 34 35 if (_canExecute != null) 36 CommandManager.RequerySuggested -= value; 37 } 38 } 39 40 [DebuggerStepThrough] 41 public Boolean CanExecute(Object parameter) 42 { 43 return _canExecute == null ? true : _canExecute(); 44 } 45 46 public void Execute(Object parameter) 47 { 48 _execute(); 49 } 50 #endregion 51 }
SongViewModel相比之前增加的代码:
SongViewMode
1 #region Commands 2 void UpdateArtistNameExecute() 3 { 4 ++_count; 5 ArtistName = string.Format("Elvis ({0})", _count); 6 } 7 8 bool CanUpdateArtistNameExecute() 9 { 10 return true; 11 } 12 13 public ICommand UpdateArtistName { get { return new RelayCommand(UpdateArtistNameExecute, CanUpdateArtistNameExecute); } } 14 #endregion
页面绑定代码:
前台
1 <Window x:Class="Example3.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:local="clr-namespace:Example3" 5 Title="Example 3" Height="350" Width="525" SizeToContent="WidthAndHeight" ResizeMode="NoResize"> 6 <Window.DataContext> 7 <!-- Declaratively create an instance of our SongViewModel --> 8 <local:SongViewModel /> 9 </Window.DataContext> 10 <Grid> 11 <Grid.RowDefinitions> 12 <RowDefinition Height="Auto" /> 13 <RowDefinition Height="Auto" /> 14 <RowDefinition Height="Auto" /> 15 <RowDefinition Height="Auto" /> 16 </Grid.RowDefinitions> 17 <Grid.ColumnDefinitions> 18 <ColumnDefinition Width="Auto" /> 19 <ColumnDefinition Width="Auto" /> 20 </Grid.ColumnDefinitions> 21 <Menu Grid.Row="0" Grid.ColumnSpan="3"> 22 <MenuItem Header="Test"> 23 <MenuItem Header="Update Artist" Command="{Binding UpdateArtistName}" /> 24 </MenuItem> 25 </Menu> 26 <Label Grid.Column="0" Grid.Row="1" Content="Example 3 - using ICommand!" /> 27 <Label Grid.Column="0" Grid.Row="2" Content="Artist: " /> 28 <Label Grid.Column="1" Grid.Row="2" Content="{Binding ArtistName}" /> 29 <Button Grid.Column="1" Grid.Row="3" Name="ButtonUpdateArtist" Content="Update Artist Name" Command="{Binding UpdateArtistName}" /> 30 </Grid> 31 </Window>
在这个例子中menu控件和button控件的命令属性都进行了绑定。
例子4:
到现在为止,你可能意识到了上面的很多东西都是重复的代码:raise INotifyPropertyChanged接口以及create command。这些非常样板化,对于INotifyPropertyChanged接口,我们也可以把它移到基类(我们叫它ObservableObject)。对于RelayCommand类,我们刚刚将它放到了我们自己的.NET库里。
例子4是把这些内容移到了一个.NET类库里用于复用。
例子5:
为了在View里(比如XAML)显示数据集合,需要使用ObservableCollection。在这个例子中,我们创建AlbumViewModel,这是一个专辑VM。
你的初次尝试可能是这样的:
AlbumViewModel
1 class AlbumViewModel 2 { 3 #region Members 4 ObservableCollection<Song> _songs = new ObservableCollection<Song>(); 5 #endregion 6 }
本例中页面的DataContext绑定AlbumViewModel,有一个list控件的ItemSource绑定这个VM的Songs。另外还有更多的ICommands,将他们绑定给一些buttons。
在这个例子中,点击“Add Artist”运行正常。但是如果点击“Update Artist Names”,失败。为了解决这个问题,需要使用SongViewModel。
例子6:
最后,我们修改AlbumViewModel包含SongViewModel的ObservableCollection,如下:
AlbumViewModel
1 class AlbumViewModel 2 { 3 #region Members 4 ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>(); 5 #endregion 6 // code elided for brevity 7 }
XAML的后台代码是完全空的!
需要注意的是,如果你在XAML中宣称你的ViewModel,不可以传递任何参数;换句话说,你的ViewModel必须有一个隐式的或显式的默认构造函数。那么如何添加为你的ViewModel添加状态呢?你会发现在Code-behind中宣称ViewModel可以传递构造参数。