简介框架
最近微软推出了UWA,又是一波新的C#+xaml学习热。好多小伙伴都对MVVM感受很好奇,可是有些地方也有点难以理解。特地写了这边文章,但愿对你有帮助。 函数
这边文章会很长,因此我会用几个例子的形式来展现一个小型MVVM框架的诞生以及怎样使用。全部的例子基于.net 4.0,使用的开发工具是Visual Studio Community 2013。 工具
基础知识学习
1.对WPF而言最重要的一个点就是数据绑定(data binding)。简单来讲,就是你有一堆数据,他们是一种类型的集合,你须要将它们展现给你的用户。因此你能够经过数据绑定的绑定到XAML上。 开发工具
2.WPF的单个界面(也就是View,一般状况下以*Window或者*Page命名)由两部分组成,它们分别是XAML和CS格式的文件。XAML设计咱们的界面和动画特效等,CS写咱们的后台代码。 动画
3.一般意义下MVVM是Model,View,ViewModel的缩写。而用这个的目的就是一个解耦的思想,也就是界面和业务逻辑的分离。固然理想状态下,咱们是但愿View中不要写代码的,因此咱们尽可能向View中没有代码这个目的靠近。 this
关键的3个点spa
1.必须使用ObservableCollection<T>来声明这个数据集合,不能使用ListT<T>或者Dictionary<TKey,TValue>。Observable意味着MVVM中的View能够观察你的集合对象。当咱们数据集合变化时,界面会发生相应的变化。 .net
2.针对于1中所描述的T,咱们必需要实现一个INotifyPropertyChanged的接口,这样咱们的属性改变时,才会通知界面。 设计
3.每个WPF中的控件都有一个DataContext属性,集合控件会有一个ItemSource的属性,这些属性均可以让咱们去绑定数据。
好了,我假设你已经有了一个大体的印象了,那接下来咱们开始咱们的第一个例子。
Example 1:数据可以展现,可是没法更新
咱们第一个例子会用一个Song的类,它看起来是下面代码这样的:
1 public class Song 2 { 3 #region 字段 4 string _artistName; 5 string _songTitle; 6 #endregion 7 8 #region 属性 9 public string ArtistName 10 { 11 get { return _artistName; } 12 set { _artistName = value; } 13 } 14 15 public string SongTitle 16 { 17 get { return _songTitle; } 18 set { _songTitle = value; } 19 } 20 #endregion 21 }
这就是咱们MVVM中的Model,接下来咱们须要考虑将数据绑定到咱们的View上。因此接下来的重点就应该在ViewModel上,我但愿可以将ArtisName展现到界面上,因此我把ViewModel命名为SongViewModel,它的代码看上去是这样的:
1 public class SongViewModel 2 { 3 public SongViewModel() 4 { 5 _song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" }; 6 } 7 8 #region 字段 9 Song _song; 10 #endregion 11 12 #region 属性 13 public Song song 14 { 15 get { return song; } 16 set { song = value; } 17 } 18 19 public string ArtistName 20 { 21 get { return _song.ArtistName; } 22 set { _song.ArtistName = value; } 23 } 24 #endregion 25 }
接下来就是咱们最神奇的地方了,咱们要将ViewModel绑定到界面上。
咱们能够经过将后台代码的方式来:
1 SongViewModel _viewModel; 2 3 public MainWindow() 4 { 5 InitializeComponent(); 6 _viewModel = base.DataContext as SongViewModel; 7 //_viewModel = new SongViewModel(); 8 //base.DataContext = _viewModel; 9 }
固然这是被容许的,可是我想强调的是更加声明式的方式。因此我决定把代码写在XAML里:
1 <Window x:Class="Example1.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:Example1" 5 Title="Example1" Height="100" Width="300" ResizeMode="NoResize"> 6 <Window.DataContext> 7 <local:SongViewModel/> 8 </Window.DataContext> 9 <StackPanel VerticalAlignment="Center" Orientation="Horizontal"> 10 <TextBlock Text="歌手:" Margin="20"/> 11 <TextBlock Text="{Binding ArtistName}" Margin="0,20"/> 12 <Button Content="更新歌手" Click="Update_Click" Margin="20"/> 13 </StackPanel> 14 </Window>
咱们声明了咱们的SongViewModel,也在TextBlock中绑定了ArtistName的属性。同时写了一个更新的时间,看下咱们的后台代码:
1 SongViewModel _viewModel; 2 3 public MainWindow() 4 { 5 InitializeComponent(); 6 _viewModel = base.DataContext as SongViewModel; 7 //_viewModel = new SongViewModel(); 8 //base.DataContext = _viewModel; 9 } 10 11 private void Update_Click(object sender, RoutedEventArgs e) 12 { 13 //界面不会更新 14 _viewModel.ArtistName = "中孝介"; 15 }
咱们能够试着跑一下,界面上很正常的显示了咱们绑定的属性,可是我写的更新按钮却没有正常的工做。
好了咱们第一个例子就结束了,下一个例子中能给咱们解决更新的问题。
Example 2:解决1中的问题,实现INotifyPropertyChanged接口
在例子1中咱们成功将数据绑定到了界面上,可是却没法更新,那是由于咱们没有实现通知接口。好了,咱们接下来给ViewModel实现这个接口。
1 public class SongViewModel : INotifyPropertyChanged 2 { 3 public SongViewModel() 4 { 5 _song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" }; 6 } 7 8 #region 字段 9 Song _song; 10 #endregion 11 12 #region 属性 13 public Song song 14 { 15 get { return song; } 16 set { song = value; } 17 } 18 19 public string ArtistName 20 { 21 get { return _song.ArtistName; } 22 set 23 { 24 _song.ArtistName = value; 25 RaisePropertyChanged("ArtistName"); 26 } 27 } 28 #endregion 29 30 #region INotifyPropertyChanged属性 31 public event PropertyChangedEventHandler PropertyChanged; 32 #endregion 33 34 #region 方法 35 private void RaisePropertyChanged(string propertyName) 36 { 37 PropertyChangedEventHandler handler = PropertyChanged; 38 if(handler != null) 39 { 40 handler(this, new PropertyChangedEventArgs(propertyName)); 41 } 42 } 43 #endregion 44 }
咱们再来运行一下咱们的程序,而后点击更新按钮,如咱们过预料的,它有效了。
到目前为止,彷佛一切都工做起来了,可是这并非咱们使用MVVM的正确方式。正如我在开始说的,MVVM的目的是为了解耦,分离界面和业务逻辑,因此咱们要尽量的在View后台不写代码。可是这个例子中,咱们将更新ViewModel的代码写在了View里,这是不对的,下一个例子中,咱们要经过命令(Command)的来将Button的事件分离出来。
Example 3:更好的实现事件,经过命令的手段
WPF提供了一个很好的方式来解决事件绑定的问题--ICommand。不少控件都有Command属性(若是没有,咱们能够将命令绑定到触发器上面,固然,这超出了这篇文章的篇幅)。接下来咱们来先实现一个ICommand接口。
ICommand须要用户定义两个方法bool CanExecute和void Execute。第一个方法能够可让咱们来判断是否能够执行这个命令,第二个方法就是咱们具体的命令。
1 public class RelayCommand : ICommand 2 { 3 4 #region 字段 5 6 readonly Func<Boolean> _canExecute; 7 readonly Action _execute; 8 9 #endregion 10 11 #region 构造函数 12 public RelayCommand(Action execute) 13 : this(execute, null) 14 { 15 } 16 17 public RelayCommand(Action execute, Func<Boolean> canExecute) 18 { 19 20 if (execute == null) 21 throw new ArgumentNullException("execute"); 22 _execute = execute; 23 _canExecute = canExecute; 24 } 25 26 #endregion 27 28 #region ICommand的成员 29 30 public event EventHandler CanExecuteChanged 31 { 32 add 33 { 34 35 if (_canExecute != null) 36 CommandManager.RequerySuggested += value; 37 } 38 remove 39 { 40 41 if (_canExecute != null) 42 CommandManager.RequerySuggested -= value; 43 } 44 } 45 46 [DebuggerStepThrough] 47 public Boolean CanExecute(Object parameter) 48 { 49 return _canExecute == null ? true : _canExecute(); 50 } 51 52 public void Execute(Object parameter) 53 { 54 _execute(); 55 } 56 57 #endregion 58 }
咱们再在咱们的ViewModel中声明一个ICommand字段:
1 #region 命令 2 void UpdateArtistNameExecute() 3 { 4 this.ArtistName = "中孝介"; 5 } 6 7 bool CanUpdateArtistNameExecute() 8 { 9 return true; 10 } 11 12 public ICommand UpdateArtistName { get { return new RelayCommand(UpdateArtistNameExecute, CanUpdateArtistNameExecute); } } 13 14 #endregion
最后,咱们再将事件绑定上这个Command:
1 <Button Content="更新歌手" Margin="20" Command="{Binding UpdateArtistName}"/>
运行一下,嗯,咱们成功将事件分离了出来。
好了,彷佛目前为止咱们已经很好的解决了全部的问题。咱们的数据,事件都是绑定的,实现了界面的完美分离。嗯,可是咱们考虑下,咱们可否把MVVM提取出来做为一个框架,来去更好的解决问题。
Example 4:更好的解决问题,提取MVVM
在上一个例子中,咱们已经解决了全部的问题了,这个例子中,咱们将上面的写好的函数提取出来。
我把上面的函数提取为两个主要的文件:ObserableObject和RelayCommand,由于代码和上面的相似,因此再也不贴出,能够直接去看源码。
Examle 5:使用ObservableCollection
前面咱们都是使用单个的Song,接下来咱们尝试使用多个Song。按照咱们一开始所说的,咱们须要一个ObservableCollection的集合。咱们用一个新的ViewModel--AlbumViewModel:
1 public class AlbumViewModel 2 { 3 #region 字段 4 ObservableCollection<Song> _songs = new ObservableCollection<Song>(); 5 #endregion 6 7 #region 属性 8 public ObservableCollection<Song> songs 9 { 10 get { return _songs; } 11 set { _songs = value; } 12 } 13 #endregion 14 15 public AlbumViewModel() 16 { 17 _songs.Add(new Song() { ArtistName = "陈奕迅", SongTitle = "十年" }); 18 _songs.Add(new Song() { ArtistName = "周杰伦", SongTitle = "发如雪" }); 19 _songs.Add(new Song() { ArtistName = "蔡依林", SongTitle = "日不落" }); 20 } 21 22 #region 命令 23 24 void AddAlbumArtistExecute() 25 { 26 _songs.Add(new Song { ArtistName = "阿桑", SongTitle = "一直很安静" }); 27 } 28 29 bool CanAddAlbumArtistExecute() 30 { 31 return true; 32 } 33 34 void UpdateAlbumArtistsExecute() 35 { 36 37 foreach (var song in _songs) 38 { 39 song.ArtistName = "Unknow"; 40 } 41 } 42 43 bool CanUpdateAlbumArtistsExecute() 44 { 45 return true; 46 } 47 48 public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } 49 50 public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } } 51 52 #endregion
咱们实现了两个命令,一个是新增歌手,一个是把全部集合里的SongTitle更改成Unknow。
而后咱们把这个ViewModel绑定到界面上:
<Window x:Class="Example5.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Example5" Title="Example5" Height="300" Width="300" ResizeMode="NoResize"> <Window.DataContext> <local:AlbumViewModel/> </Window.DataContext> <StackPanel Orientation="Horizontal"> <ListView ItemsSource="{Binding songs}" Width="200"> <ListView.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Label Content="{Binding ArtistName}" /> <Label Content="{Binding SongTitle}" FontSize="10" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <StackPanel> <Button Content="新增歌手" Height="40" Margin="20" Command="{Binding AddAlbumArtist}"/> <Button Content="更新歌手" Height="40" Margin="20" Command="{Binding UpdateAlbumArtists}"/> </StackPanel> </StackPanel> </Window>
当咱们运行程序的时候,咱们发现咱们的新增功能是正常工做的,可是咱们的更新功能却没有成功把字段更改成Unkown。
这是能够理解的。为何?还记得开始咱们说的T须要作的吗?由于咱们并未有给Song实现INotifyChanged接口,它的属性变化是不会引发界面的变动的。那么咱们须要给Song实现这个接口吗?咱们经过这样作能实现功能,可是咱们不推荐这么作。下一个例子中,咱们将经过多加一个ViewModel来解决这个问题。
Example 6:两个ViewModel,解决Model属性改变问题
上个例子中,咱们没法经过改变Model的属性来实现界面的更改。因此咱们引入第二个ViewModel来解决问题。咱们新建一个SongViewModel:
1 public class SongViewModel : ObservableObject 2 { 3 public SongViewModel() 4 { 5 _song = new Song() { ArtistName = "Unknow", SongTitle = "Unknow" }; 6 } 7 8 #region 字段 9 Song _song; 10 #endregion 11 12 #region 属性 13 public Song song 14 { 15 get { return song; } 16 set { song = value; } 17 } 18 19 public string ArtistName 20 { 21 get { return _song.ArtistName; } 22 set 23 { 24 _song.ArtistName = value; 25 RaisePropertyChanged("ArtistName"); 26 } 27 } 28 29 public string SongTitle 30 { 31 get { return _song.SongTitle; } 32 set 33 { 34 _song.SongTitle = value; 35 RaisePropertyChanged("SongTitle"); 36 } 37 } 38 #endregion 39 }
而后咱们用这个ViewModel来更改AlbumViewModel:
1 public class AlbumViewModel 2 { 3 #region 字段 4 ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>(); 5 #endregion 6 7 #region 属性 8 public ObservableCollection<SongViewModel> songs 9 { 10 get { return _songs; } 11 set { _songs = value; } 12 } 13 #endregion 14 15 public AlbumViewModel() 16 { 17 _songs.Add(new SongViewModel() { ArtistName = "陈奕迅", SongTitle = "十年" }); 18 _songs.Add(new SongViewModel() { ArtistName = "周杰伦", SongTitle = "发如雪" }); 19 _songs.Add(new SongViewModel() { ArtistName = "蔡依林", SongTitle = "日不落" }); 20 } 21 22 #region 命令 23 24 void AddAlbumArtistExecute() 25 { 26 _songs.Add(new SongViewModel { ArtistName = "阿桑", SongTitle = "一直很安静" }); 27 } 28 29 bool CanAddAlbumArtistExecute() 30 { 31 return true; 32 } 33 34 void UpdateAlbumArtistsExecute() 35 { 36 37 foreach (var song in _songs) 38 { 39 song.ArtistName = "Unknow"; 40 } 41 } 42 43 bool CanUpdateAlbumArtistsExecute() 44 { 45 return true; 46 } 47 48 public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } 49 50 public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } } 51 52 #endregion 53 }
咱们无需更改界面上任何绑定的东西,直接运行咱们的程序,这样咱们发现就能工做了。
到此为止,一个基本的MVVM模型就已经基本完成了。下一个例子咱们演示如何在Command中传参数。
(扩展)Example 7:Command传参数
咱们把上面例子中的更新歌手改成更新选中歌手。这样咱们就须要只更改选中的歌手的值。咱们须要更改界面上的绑定,来将选中的选做为传参传到Command:
1 <Button Content="更新选中歌手" Height="40" Margin="20" Command="{Binding UpdateAlbumArtists}" CommandParameter="{Binding ElementName=lv,Path=SelectedItem}"/>
而后修改咱们的AlbumViewModel中的Command:
1 void UpdateAlbumArtistsExecute(SongViewModel song) 2 { 3 if(song == null) return; 4 5 song.ArtistName = "Unknow"; 6 } 7 8 bool CanUpdateAlbumArtistsExecute(SongViewModel song) 9 { 10 return true; 11 } 12 13 public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } 14 15 public ICommand UpdateAlbumArtists { get { return new RelayCommand<SongViewModel>(new Action<SongViewModel>(UpdateAlbumArtistsExecute), new Predicate<SongViewModel>(CanUpdateAlbumArtistsExecute)); } }
这样咱们很容易就实现了效果:
结束语:
本篇文章对MVVM的一些基本概念作了一些演示,可是仍是有一些缺失,好比说控件没有Command属性时如何处理事件。只是但愿能对初学者起到必定的帮助。
最后,感谢你能看到最后。
源代码下载:http://files.cnblogs.com/files/youngytj/WPFMVVMDemo.zip