原文地址 http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial
簡介
先假設大家對c#已經有一定的瞭解了,並且很容易接受一些關於WPF的知識。因爲下面的知識是通過WPF爲例的。
我前段時間開始研究了一下WPF,但是找不到什麼有幫助的關於MVVM的教程。因此我希望這篇文章能讓你眼前一亮。
當我們剛開始學一門新技術時,我們得益於前人的積累。但是從我個人的觀點來看,我看過的那些教程什麼的都不能滿足我,原因如下:
例子都是用XAML寫的例子根本不提那些能大大簡化你開發過程的特性和關鍵點那些例子的存在只是爲了向你炫耀 WPF/XAML對一些無聊特性的兼容性在例子中有一些變量的名字很容易和語言本身的一些關鍵詞和類混淆,這讓我們這些新手們很蛋疼而我現在寫的這玩意卻沒有這些缺點,它是根據我在谷歌搜到的一篇很火的文章改寫而來。這篇文章可能不是100%正確,也可能只是許多最佳實踐中的一種,但它確實很好地說明了一些我在這幾個月裏發現的重點。
好吧我們節奏緊湊一點,先說一個關鍵點然後看幾個例子。P.S. 這個UI比較醜,但那不是重點!另外這篇教程比較長,我已經省略了許多代碼,所以請下載源代碼來觀看例子。
一些你要了解的關鍵點
1. 你應該用ObservableCollection<>而不是List或者Dictionary去存儲數據。顧名思義,你的界面必須能Observe(監視)你的數據集。而恰好這個Collection實現了一些能夠讓它被很好地監控的接口。
2. 每一個WPF控件(包括Window)都有一個DataContext屬性,而每一個Collection控件都有一個ItemsSrouce屬性用於綁定數據。
3. INotifyPropertyChanged接口將被廣泛地使用在你的界面和後臺代碼中,它用於傳遞數據的變更。
Example 1:一個大部分人都這樣做的錯誤做法
來個栗子先! 我們先創建一個Song類,而不是那個二逼的Person類。我們能把Song組織到Album中,甚至更大的集合中。一個簡單的song類如下:
public class Song
{
#region Members
string _artistName;
string _songTitle;
#endregion
#region Properties
/// The artist name.
public string ArtistName
{
get { return _artistName; }
set { _artistName = value; }
}
/// The song title.
public string SongTitle
{
get { return _songTitle; }
set { _songTitle = value; }
}
#endregion
}
在WPF的術語中這叫做Model,而圖形界面就是我們的View。而ViewModel就是將數據綁定到他們上的魔法師,它把一個簡單的Model變成了WPF框架能夠使用的東西。我再重申一下,這個類就是我們的Model。
好的現在我們來創建一個SongViewModel。我應該思考的是,我們要展示什麼東西出來。假設我們只關心一首歌的演唱者,那這個SongViewModel就可以被定義成這樣:
public class SongViewModel
{
Song _song;
public Song Song
{
get
{
return _song;
}
set
{
_song = value;
}
}
public string ArtistName
{
get { return Song.ArtistName; }
set { Song.ArtistName = value; }
}
}
差不多就是這樣。我們在ViewModel中暴露了一個屬性,就是想在UI中自動地改變它,反之亦然:
SongViewModel song = ...;
// ... enable the databinding ...
// change the name
song.ArtistName = "Elvis";
// the gui should change
還有一點要說的就是,我們在XAML中這樣來創建我們的ViewModel:
<Window x:Class="Example1.MainWindow"
xmlns:local="clr-namespace:Example1">
<Window.DataContext>
<!-- Declaratively create an instance of our SongViewModel -->
<local:SongViewModel />
</Window.DataContext>
這相當於我們在後臺代碼中這樣寫:
public partial class MainWindow : Window
{
SongViewModel _viewModel = new SongViewModel();
public MainWindow()
{
InitializeComponent();
base.DataContext = _viewModel;
}
}
當然如果你想用後臺代碼實現的話你得把XAML中的Window.DataContext去掉:
<Window x:Class="Example1.MainWindow"
xmlns:local="clr-namespace:Example1">
<!-- no data context -->
好了,這就是我們的界面:
點那個Button沒用,因爲我們還沒綁定數據呢。
數據綁定
還記得我說過要選一個用來展示的屬性麼?這個屬性就是ArtistName,我選這個屬性是因爲WPF的屬性中沒有 和它名字相同的屬性。(之後是作者的一堆吐槽,不表。)
要把ArtistName屬性綁定到SongViewModel,我們只要在Xaml中寫:
<Label Content="{Binding ArtistName}" />
關鍵字 “Binding”會把Label的Content和在DataContext中存在的ArtistName的值綁定起來。如你所見,我們把DataContext設爲SongViewModel的一個實例,然後就可以在Label中成功地展示_songViewModel.ArtistName了。
但是,你現在點button還是沒反應,因爲界面接受不到任何關於數據改變的通知。
Example 2:INotifyPropertyChanged接口
我們現在得實現這個INotifyPropertyChanged(簡稱INPC)接口了。任何實現這個接口的類,都會在值改變的時候發通知給相應監聽者。所以我們要改動一下SongViewModel類:
public class SongViewModel : INotifyPropertyChanged
{
#region Construction
/// Constructs the default instance of a SongViewModel
public SongViewModel()
{
_song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
}
#endregion
#region Members
Song _song;
#endregion
#region Properties
public Song Song
{
get
{
return _song;
}
set
{
_song = value;
}
}
public string ArtistName
{
get { return Song.ArtistName; }
set
{
if (Song.ArtistName != value)
{
Song.ArtistName = value;
RaisePropertyChanged("ArtistName");
}
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Methods
private void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
有幾點要提一下。首先,我們有檢查屬性的值是否真的改變了,這樣能使項目的性能在數據很複雜時稍稍提升。其次,當值真的改變的時候,我們會發出PropertyChanged時間的信號給所有監聽者。
現在我們有了Model和ViewModel,只要再定義一下View就行。這就是我們的MainWindow:
<Window x:Class="Example2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Example2"
Title="Example 2" SizeToContent="WidthAndHeight" ResizeMode="NoResize"
Height="350" Width="525">
<Window.DataContext>
<!-- Declaratively create an instance of our SongViewModel -->
<local:SongViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Example 2 - this works!" />
<Label Grid.Column="0" Grid.Row="1" Content="Artist: " />
<Label Grid.Column="1" Grid.Row="1" Content="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist"
Content="Update Artist Name" Click="ButtonUpdateArtist_Click" />
</Grid>
</Window>
要測試數據綁定了,我們先沿用傳統方法:實現button的Onclick函數:
public partial class MainWindow : Window
{
#region Members
SongViewModel _viewModel;
int _count = 0;
#endregion
public MainWindow()
{
InitializeComponent();
// We have declared the view model instance declaratively in the xaml.
// Get the reference to it here, so we can use it in the button click event.
_viewModel = (SongViewModel)base.DataContext;
}
private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
{
++_count;
_viewModel.ArtistName = string.Format("Elvis ({0})", _count);
}
}
這行得通,但我們不該這樣寫。首先,我們把更新歌手的邏輯放到了界面的後臺代碼中,但它不應該寫在這!這些代碼應該只和Window這個界面有關。第二個問題是,如果我們想把onclick中的代碼放到別的地方,例如從一個Menu中選擇,這就意味着我們得複製粘貼好多次。
這是我們點擊按鈕後的界面:
Example 3:Commands
在UI中綁定事件有點麻煩。但是WPF提供了一種好方法:ICommand。許多控件都有Command屬性,它和Content,ItemsSource的綁定規則一樣,除非你需要綁定到一個能夠返回一個ICommand的屬性。
在我們這個碉堡的例子中,我僅僅實現了一個碉堡的類叫RelayCommand,它實現了ICommand接口。
ICommand要求用戶定義兩個方法:bool CanExecute 和 void Execute。前者告訴用戶是否能夠執行,它能夠用來控制空間是否可用。在我們的例子中,我們不關心這個,所以我們只返回true,表示我們一直能調用Execute成功。
因爲我們想重用ICommand的代碼,所以就把重複代碼放到RelayCommand中。具體的代碼可以看壓縮包。這是界面:
Example 4:Frameworks
現在你可能覺得許多代碼都是重複的,實現INPC接口,構造Command。其實這些都是些模版代碼,例如我們可以把實現INPC的代碼放到一個ObservableObject基類中。而對於RelayCommand類,我們把它放入我們的.Net類庫中。這就是你在網上找到的那些MVVM框架做的事(例如Prism Caliburn等等)。
ObservableObject和RelayCommand這兩個類是重構之後必然能得到的比較基本的類。所以我把這些類放到一個小的類庫中希望能夠在以後重用。現在界面是這樣:
Example 5: 歌曲的集合,但是這種做法是錯的
我之前說過,要想在View(就是你的XAML)中顯示數據集,必須把數據放到ObservableCollection中。本例我們創建一個AlbumViewModel類,它把許多個Song聚集起來。我們還構建了一個SongDatabase用於歌曲信息的生成。
這是我們的AlbumViewModel原型:
class AlbumViewModel
{
#region Members
ObservableCollection<Song> _songs = new ObservableCollection<Song>();
#endregion
}
你應該會想“這次我的View Model不一樣了,我想把songs用一個AlbumViewModel來展示,而不是一個SongViewModel”。
我們還要創建一些ICommand並且把他們綁定到button上。
public ICommand AddAlbumArtist {}
public ICommand UpdateAlbumArtists {}
這個例子中,當你點擊Add Artist時一切ok,但是點擊Update的時候卻沒反應。你可以去讀一下MSDN上的這一頁,高亮的黃字說:
爲了完全支持數據綁定,你的集合衆的每一個屬性都應該實現一個合適的變值提醒機制,例如INotifyPropertyChanged接口。
現在我們的界面是這樣:
Example 6: 歌曲的集合,正確做法!
在這個最終的例子中,我們修復了AlbumViewModel,讓ObservableCollection中包含的變量變成了SongViewModels:
class AlbumViewModel
{
#region Members
ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>();
#endregion
// code elided for brevity
}
現在我們所有的button都綁定到相應的command上了,後臺代碼真乾淨!
最終界面是這樣:
結論
實例化你的ViewModel
最後一點要說的是當我們在XAML中聲明ViewModel時,你沒法去傳遞參數。換句話說,你的ViewModel必須沒有,或者說有一個默認的無參構造函數。
當然你也可以在cs代碼中實例化ViewModel並且傳參給他。
其他框架
還有許多不同的MVVM框架,他們有不同的複雜度,不同的功能,不同的目標技術例如WPF Wp7 Silverlight或三者共有。
最後
希望這6個例子能夠讓你輕鬆寫出MVVM框架的WPF應用。我已經嘗試把我認爲的重點都在這篇文章中覆蓋了。
引用
- Effective C#
- Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
- C# 4.0 in a Nutshell: The Definitive Reference
- WPF 4 Unleashed
- Josh Smith
高級閱讀
Hello World in MVVM Light in 10 MinutesMVVMLight Using Two Views