WPF/MVVM快速指引

簡介框架

最近微軟推出了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

相關文章
相關標籤/搜索