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 CanExecutevoid 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可以传递构造参数。
posted @ 2013-03-18 22:02  BlueMountain_79  阅读(605)  评论(0编辑  收藏  举报