WPF/MVVM 快速開始指南(譯)(轉)


本篇文章是Barry Lapthorn創作的,感覺寫得很好,翻譯一下,做個紀念。由於英文水平實在太爛,所以翻譯有錯或者譯得不好的地方請多指正。另外由於原文是針對WPF的,我在原文的基礎上做了一些修改,讓例子能在silverlight上運行。

原文鏈接:http://www.codeproject.com/KB/WPF/WpfMvvmQuickStart.aspx

中文原文:http://www.cnblogs.com/xiaobaihome/archive/2011/11/28/2266536.html

簡介

假設你對C++有很好的理解,也對C#有適當的了解,那么准備開始WPF學習將不會太困難。我在六個月前開始着手於WPF,然后可能在谷歌搜索中導致了一個結果,我最后開始明白並且開始使用WPF進行生產(我是否錯過了這條船,是另一天的另一個故事了)。

就像學習任何新技術一樣,事后你將從中得到益處,在我看來,幾乎我所遇到的所有的WPF教程都有以下幾個方面的不足:

  • 例子全是WPF里面的
  • 例子缺少關鍵的注釋,事實上你自己制作一個更容易
  • 例子試圖炫耀WPF的能力,大量的毫無意義的結果,對你沒有任何幫助
  • 例子使用的類名中有些屬性似乎和框架關鍵字或類名太相似,因此很難在xaml代碼中作為用戶定義的標示符來使用(ListBoxGroupStyle的名稱很讓初學者頭疼)。

為了解決這個問題,基於在谷歌上輸入“WPF 教程”得到的第一條結果我寫下了這篇文章。這篇文章可能不是100%正確,或者甚至是做事情的“唯一正確的方法”,不管怎么樣它將闡明一些主要的點,這些點是我在六個月前希望發現的。

我將快速的介紹一些主題,然后展示一個例子來解釋或演示每一個觀點。因此,我事實上沒有試圖使GUI更漂亮,因為這不是這篇文章的要點(參見上面的要點)。

因為這個教程相當長,為了簡潔我將省略許多代碼,因此請下載附加的ZIP文件,然后看里面的例子(.NET4.0/VS2010)。每一個例子都是建立在前一個例子上的。

基本要素

  1. WPF最給力的就是數據綁定,簡單的說,你有一些數據,按照某種特征分類放在一個集合里,然后你想將它顯示給用戶。你可以將數據“綁定”到xaml代碼。
  2. WPF有兩個部分,xmal描述你的GUI布局和效果,這個后台代碼是綁定到xaml的。
  3. 一種最優雅的和最大可能被復用的方式來組織你的代碼的方法是使用"MVVM"模式:模型,視圖,視圖模型。

你需要知道的關鍵點

  1. 存儲數據你應該使用的集合是ObservableCollection<>。而不是list,也不是dictionary,而是 ObservableCollection。“Observable”這個詞在這里是為這種情況提供:WPF窗口需要能觀察到你的數據集合。這個集合類實 現了WPF使用的幾個接口。
  2. 每一個WPF控件(包括“窗口”)都有一個“DataContext”,集合控件都有一個“ItemsSource”屬性用於綁定。
  3. “INotifyPropertyChanged”接口將被廣泛的的用於GUI和你的代碼之間的通信,當數據有任何改變的時候。

例1:錯誤的做法

開始的最好方法是從例子開始,我們將從一個song類開始,而不是通常的person類,我們可以將歌曲整理到專輯里面,或者是一個大的集合里,或者按藝術家來整理。一個簡單的song類應該會像下面這樣:

復制代碼
 public class Song
{
#region Members
string _artistName;
string _songTitle;
#endregion

#region Properties
///<summary>
/// 藝術家名稱
///</summary>
public string ArtistName
{
get { return _artistName; }
set { _artistName = value; }
}

///<summary>
/// 歌曲標題
///</summary>
public string SongTitle
{
get { return _songTitle; }
set { _songTitle = value; }
}
#endregion
}
復制代碼

在WPF術語中,這個叫“模型”,GUI是“視圖”。不可思議的是“視圖模型”,通過數據綁定將它們綁在一起,它真的是一個很好的適配器能將模型變成某種WPF框架可以使用的東西。所以只是重復一下,這就是“模型”。

自我們創建Song作為引用類型以后,由於副本在內存上很便宜,我們可以非常容易的創建SongViewMode。我們首先需要考慮的是,什么是我們(潛在的)需要顯示的?假如我們只關心歌曲的藝術家名稱,而不關心歌曲的標題,那么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里暴露了一個屬性,我們顯然會想使在代碼里改變的歌曲的藝術家名稱自動的顯示在GUI上,反之亦然:

SongViewModel song = ...;
//...允許數據綁定...
//改變名稱
song.ArtistName = "Elvis";
//gui應該發生改變

請注意,在所有的例子里面,我們都創建了視圖模型的“聲明”,例如,我們在xaml代碼里這樣做:

復制代碼
<UserControl x:Class="Example1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<UserControl.DataContext>
<!--聲明創建一個SongViewModel的實例-->
<local:SongViewModel/>
</UserControl.DataContext>

<Grid x:Name="LayoutRoot" Background="White">

</Grid>
</UserControl>
復制代碼

這等價於在后台代碼MainWindo.cs里這樣做:

復制代碼
    public partial class MainWindow : UserControl
{
SongViewModel _viewModel = new SongViewModel();
public MainWindow()
{
InitializeComponent();
base.DataContext = _viewModel;
}
}
復制代碼

然后移除xaml中的DataContext元素:

復制代碼
<UserControl x:Class="Example1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<!--無數據上下文-->
復制代碼

 這是運行結果:

點擊更新按鈕不會進行任何更新,因為我們沒有實現數據綁定。

數據綁定

記得我在一開始說,我會選擇一個突出的屬性。 在這個例子里,我們想顯示藝術家姓名。我選擇這個名字是因為它和任何WPF屬性都不相同。在網絡上有無數的例子,選擇一個Person類和他的Name屬性(它在多個WPF元素中都存在),也許這些文章的作者只是沒有意識到這樣對初學者來說特別容易混淆(這些文章的目標讀者有足夠的好奇心)。

有許多的其它關於數據綁定的文章,因此我不能全部包括他們。我希望這個例子是如此微不足道,以至於你可以看清楚它是怎么回事。

綁定SongViewModel的ArtistName屬性,我們可以簡單的這么做在MianWindow.xaml中:

<TextBlock Text="{Binding ArtistName}"/>

“綁定”關鍵字綁定這個控件的Text,在這里這個控件是TextBlock,對“ArtistName”這個屬性來說由DataContext返回對象。就像你上面所看到的,我們給DataContext設置了一個SongViewModel實例,因此我們在TextBlock上實際是顯示的_songViewModel.ArtistName。

再說一次:點擊更新按鈕不會進行任何更新,因為我們沒有實現數據綁定。GUI不會收到任何的關於屬性改變的通知。

例2:INotifyPropertyChanged接口

我們必須實現名稱為INotifyPropertyChanged的巧妙接口。就像他說的,任何實現了這個接口的類,當屬性發生改變的時候會通知所有監聽者,所以我們需要修改SongViewModel類稍微多一點點:

復制代碼
public class SongViewModel : INotifyPropertyChanged
{

#region 構造函數
/// <summary>
/// 構造缺省的SongViewModel實例
/// </summary>
public SongViewModel()
{
_song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
}
#endregion

#region 成員
Song _song;
#endregion

#region 屬性
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 成員

public event PropertyChangedEventHandler PropertyChanged;

#endregion

#region INotifyPropertyChanged 方法
private void RaisePropertyChanged(string propertyName)
{
//得到一個副本以預防線程問題
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
復制代碼

現在這里有幾件事發生了。首先,我們檢查了我們是否真的改變了屬性:這樣對大多數復雜對象來說能稍微提高性能。第二,如果值已經改變,我們向所有監聽者注冊PropertyChanged事件。

那么現在我們有了一個模型,和一個視圖模型。我們只需要在定義視圖。只需要修改MainWindow:

復制代碼
<UserControl x:Class="Example2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example2"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<UserControl.DataContext>
<!--聲明創建一個SongViewModel的實例-->
<local:SongViewModel/>
</UserControl.DataContext>

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="例2 - 它能工作!" />
<TextBlock Grid.Column="0" Grid.Row="1" Text="藝術家: " />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist" Content="更新藝術家姓名" Click="ButtonUpdateArtist_Click" />
</Grid>
</UserControl>
復制代碼

為了測試數據綁定,我們可以用傳統方法,創建一個按鈕然后寫上它的OnClick事件,所以上面的xaml有一個按鈕,和Click事件,給出后台代碼:

復制代碼
 public partial class MainWindow : UserControl
{
#region 成員
/// <summary>
/// 視圖模型
/// </summary>
SongViewModel _viewModel;

int _count = 0;
#endregion

public MainWindow()
{
InitializeComponent();

//已經在xaml代碼中聲明了視圖模型實例
//在這里拿到它的引用,因此我們可以在按鈕click事件里使用它
_viewModel = (SongViewModel)base.DataContext;
}

private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
{
++_count;
_viewModel.ArtistName = string.Format("Elvis({0})",_count);
}
}
復制代碼

這樣就OK啦,但是我們不該這樣使用WPF:首先,我們在后台代碼里添加了“更新藝術家”邏輯。它不應該屬於那里。窗口類與打開窗口是關聯的。第二個問題是,假如我們想移動在按鈕click事件中的邏輯,這將很難控制。例如,制作一個菜單項,那就意味着我們將要剪切,粘貼,然后在多個地方編輯。

這是改進后的視圖,現在點擊那里能工作了:

例 3:命令

綁定到GUI事件是有問題的。WPF給你提供了一個更好的方式,那就是ICommand接口。許多控件都有一個命令屬性。這些控件同樣服從綁定就像Content和ItemSource,除了你需要綁定它到一個屬性上外還要返回一個ICommand接口。對於我們在這里看到的微不足道的例子來說,我們僅僅實現了一個被稱為“RelayCommand”很小的類,它實現了ICommand接口:

復制代碼
 public class RelayCommand:ICommand
{
#region 成員
readonly Func<Boolean> _canExecute;
readonly Action _execute;
#endregion

#region 構造函數
public RelayCommand(Action execute)
:this(execute,null)
{

}

public RelayCommand(Action execute,Func<Boolean> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
_execute = execute;
_canExecute = canExecute;
}
#endregion

#region ICommand 成員

public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute();
}

public event EventHandler CanExecuteChanged;

public void Execute(object parameter)
{
_execute();
}

#endregion
}
復制代碼

 ICommand接口需要用戶定義兩個方法:bool CanExecute方法,和void Execute方法。CanExecute方法實際上僅對用戶說,我可以執行這個命令嗎?這對管理context是很有用的,你可以執行GUI的動作。在我們的例子里,我們不在意這個,所以我們返回true,意味着框架總是可以調用“Execute”方法。你可能會有一種情況,你有一個命令綁定到了按鈕上,它只能在你選擇了列表里的某一項后才能執行。你可能會在“CanExecute”方法里實現這個邏輯。
由於我們想重復使用ICommand接口的代碼,我們使用RelayCommand類,它包含了所有可以重復使用的代碼,那些我們不想繼續寫的代碼。
為了展示ICommand是多么容易被復用,我們給一個按鈕和一個菜單項都綁定了更新藝術家命令。

復制代碼
<UserControl x:Class="Example3.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Example3"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<UserControl.DataContext>
<!--聲明創建一個SongViewModel的實例-->
<local:SongViewModel/>
</UserControl.DataContext>

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Content="更新藝術家" Grid.Row="0" Grid.ColumnSpan="2" Width="80"
HorizontalAlignment="Left" Command="{Binding UpdateArtistName}"></Button>
<TextBlock Grid.Column="0" Grid.Row="1" Text="例3 - 使用ICommand接口!" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="藝術家: " />
<TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="3" Name="ButtonUpdateArtist" Content="更新藝術家姓名"
Command="{Binding UpdateArtistName}"/>
</Grid>
</UserControl>
復制代碼

注意,我們不再綁定按鈕指定的Click事件,或者菜單指定的Click事件。

復制代碼
public partial class MainWindow : UserControl
{
//注意:現在我們沒有使用控件指定的事件,我們不需要引用視圖模型
public MainWindow()
{
InitializeComponent();
}
}
復制代碼

 

點擊兩個按鈕都能工作。

例4:框架

如果你已經仔細的閱讀過這篇文章,到目前為止,你可能注意到有許多重復的代碼:注冊INPC,或創建命令。這是大多數代碼的樣板文件,對於INPC來說,我們可以將它移到一個基類里面以方便我們能“ObservableObject”。對於RelayCommand類來說,我們可以移動它到我們的.NET類庫里面。你在網上找到的所有MVVM框架(Prism,Calibum等)都是這樣開始的。
 就“ObservableObject”和“RelayCommand”類的聯系來講,他們寧願變得更基本,進行重構是必然的結果。這些類實際上幾乎和Josh Smith寫的那些類一模一樣,這一點也不讓人感到吃驚。

所以我們將這些類移動到一個小型類庫,以便於我們可以在將來復用。

這結果看起來和以前非常相似:

例5:歌曲集合,錯誤的做法

我曾經說過,為了在你的試圖(例如xaml)里顯示集合里的條目,你需要使用ObservableCollection.在這個例子里,我們創建了一個AlbumViewModel,它非常漂亮的把我們的歌曲收集到了一起在人們能理解的一些事情上.我們也引進一個簡單的歌曲數據庫,只是為了讓我們能在這個例子中快速的產生一些歌曲信息.

你的第一次嘗試可能會像下面這樣:

復制代碼
 public class AlbumViewModel
{
#region 成員
ObservableCollection<Song> _songs = new ObservableCollection<Song>();
#endregion
}
復制代碼

你可能會想:"這時候我有一個不同的試圖模型,我想作為一個AlbumViewModel顯示這些歌曲,而不是SongViewModel".

我們也創建一些更多的命令然后把他們附加到一些按鈕上:

public ICommand AddAlbumArtist {}

public ICommand UpdateAlbumArtists {}

在這個例子中,點擊"添加藝術家"會工作得很好,不過點擊"更新藝術家名稱"將不能工作.如果你讀了MSDN這一頁黃色高亮的注釋,它是這樣解釋的:

  為了充分的支持數據值從綁定的數據源對象傳遞到綁定目標,你的集合中的每一個對象,它支持可綁定的屬性必須實現一個恰當的屬性改變通知機制,例如INotifyPropertyChanged接口.

我們的視圖看起來是這樣的:

例6:歌曲集合,正確的做法

在最后這個例子中,我們把AlbumViewModel安裝到有一個ObservableCollection的SongViewModels上,以便我們更容易的創建它:

現在我們所有的按鈕都綁定到命令上來操作我們的集合,我們的后台代碼MainWindow.cs仍然是十分的干凈.

我們的視圖看起來是這樣:


結論

實例化你的視圖模型

最后值得一提的是,當你在xaml中聲明你的視圖模型的時候,你不能給它傳遞任何參數:換句話說,你的視圖模型必須有一個
隱式的或者顯示的缺省構造函數.在視圖模型上添加多少個狀態完全取決於你,你也許發現在Mainwindow.cs后台代碼里聲明視圖模型會更容易,在這里你可以傳遞構造參數.

其他的框架

有許多其他的復雜度和功能不同的MVVM框架,他們以WPF,WinPho7,Silverlight或者這三個的任何組合作為目標.

最后..

希望這6個例子能向你展示出使用MVVM寫一個WPF應用程序是多么的簡單,我試圖覆蓋所有的要點,這些要點是我認為很重要的而且經常在許多文章里被討論.

如果你發現這篇文章有用,請投一票.

如果你在這篇文章中發現了錯誤,或者我說的有不對的對方,或者你有一些關於這篇文章的其他問題,請在下面留下評論解釋為什么,以及你是怎么樣適應他的.

引用

寫這篇文章的時候我已經遵守了各種.NET編程指南約定和風格.我在編寫這篇文章的時候用到的引用列表列在這里:

  1. Effective C#
  2. Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries
  3. C# 4.0 in a Nutshell: The Definitive Reference
  4. WPF 4 Unleashed
  5. Josh Smith


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM