簡介
最近微軟推出了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中沒有代碼這個目的靠近。
關鍵的3個點
1.必須使用ObservableCollection<T>來聲明這個數據集合,不能使用ListT<T>或者Dictionary<TKey,TValue>。Observable意味着MVVM中的View可以觀察你的集合對象。當我們數據集合變化時,界面會發生相應的變化。
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