WPF-MVVM模式學習筆記2——MVVM簡單樣例
一. MVVM理解
1. 先建立一個簡單的WPF樣例,並逐步將它重構成為MVVM模式。
這個Demo需求是:在介面上放置文字框用來顯示定義的類Student中的名字,放置Button來修改Student的名字。
剛建立好的樣例工程文件如下圖:
緊接著新增一個Student類,
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMDemo { public class Student : INotifyPropertyChanged { string firstName; public string FirstName { get { return firstName; } set { firstName = value; OnPropertyChanged("FirstName"); } } string lastName; public string LastName { get { return lastName; } set { lastName = value; OnPropertyChanged("LastName"); } } public Student(string firstName, string lastName) { this.firstName = firstName; this.lastName = lastName; } void OnPropertyChanged(string propName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propName)); } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }
此時工程結構圖如下圖
然後修改 MainWindow.xaml,內容如下
<Window x:Class="MVVMDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid x:Name="gridLayout"> <Grid.ColumnDefinitions> <ColumnDefinition Width="5*" /> <ColumnDefinition Width="5*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="5*" /> <RowDefinition Height="5*" /> <RowDefinition Height="5*" /> <RowDefinition Height="5*" /> </Grid.RowDefinitions> <TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Text="{Binding Path=FirstName,Mode=TwoWay}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/> <TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBox Text="{Binding Path=LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/> <Button x:Name="BtnView" Content="I am View" Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right"/> </Grid> </Window>
下圖為MainWindow檢視
緊接著,在MainWindow.cs新增如下內容
public MainWindow() { InitializeComponent(); Student student = new Student("Wang", "WenSong"); gridLayout.DataContext = student; BtnView.Click += new RoutedEventHandler(delegate(object sender, RoutedEventArgs e) { student.FirstName = "BBK工作室"; student.LastName = "www.bigbearking.com"; }); }
此時執行程式,如下圖
點選按鈕BtnView,此時介面如下
上述程式碼工程,點此下載
2.問題來了
如果我們需要讓頁面的值和Student例項的值保持一致,則必須要讓型別繼承自INotifyPropertyChanged介面,並像下面這樣編碼:
public class Student : INotifyPropertyChanged
{
string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
OnPropertyChanged("FirstName");
}
}
string lastName;
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
OnPropertyChanged("LastName");
}
}
public Student(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
如果應用程式中存在多個這樣的型別,比如還有Teacher類,則每個類都要實現自己的OnPropertyChanged方法,這顯然是不合理的。所以,需要一個超類來包裝這種需求,當然這個超類繼承自INotifyPropertyChanged。
3.下面,在工程中新增這個超類NotificationObject,如下結構圖
這個超類的程式碼為
public abstract class NotificationObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void RaisePropertyChanged(params string[] propertyNames)
{
if (propertyNames == null) throw new ArgumentNullException("propertyNames");
foreach (var name in propertyNames)
{
this.RaisePropertyChanged(name);
}
}
protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var propertyName = ExtractPropertyName(propertyExpression);
this.RaisePropertyChanged(propertyName);
}
public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
{
throw new ArgumentNullException("propertyExpression");
}
var memberExpression = propertyExpression.Body as MemberExpression;
if (memberExpression == null)
{
throw new ArgumentException("PropertySupport_NotMemberAccessExpression_Exception", "propertyExpression");
}
var property = memberExpression.Member as PropertyInfo;
if (property == null)
{
throw new ArgumentException("PropertySupport_ExpressionNotProperty_Exception", "propertyExpression");
}
var getMethod = property.GetGetMethod(true);
if (getMethod.IsStatic)
{
throw new ArgumentException("PropertySupport_StaticExpression_Exception", "propertyExpression");
}
return memberExpression.Member.Name;
}
}
相應的,將Student型別修改為:
public class Student : NotificationObject
{
string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
//OnPropertyChanged("FirstName");
this.RaisePropertyChanged("FirstName");
}
}
string lastName;
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
//OnPropertyChanged("LastName");
this.RaisePropertyChanged("LastName");
}
}
public Student(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
這部分程式碼,點此下載
4.問題再次出現,經過修改後的Student型別,是什麼?
是實體Model,領域Model,還是別的什麼?實際上,因為沒有采用任何架構模式,當前的Student型別什麼也不是,揉雜了很多功能。它既要負責提供屬性,也要負責控制。
在MVVM架構模式中,和MVC稱謂不同的地方,就是VM(ViewModel)部分。VM負責:接受View請求並決定呼叫哪個模型構件去處理請求,同時它還負責將資料返回給View進行顯示。也就是說,VM完成的角色可以理解為MVC中的Control。(另外需要注意的一點是,在MVC中有一個概念叫做表現模型,所謂表現模型是領域模型的一個扁平化投影,不應和MVVM中的VIEW MODEL相混淆)。
所以,我們現在要明確這些概念。首先,將Student型別的功能細分化,VM的部分,我們跟頁面名稱對應起來應該叫做MainViewModel。實際專案中,功能頁面會相應名為StudentView.xaml,則對應的VM名便稱之為StudentViewModel.cs。我們繼續重構上面的程式碼。
二.建立MVVM的各個部分
現在重構程式碼,工程的結構變化比較大,我會把這部分程式碼也傳上去的。
首先,在原有的工程上建立三個資料夾 Model、View、ViewModel,如下圖
1. 領域模型DomainModel部分
然後將Student.cs移到Model資料夾內,並修改Student.cs裡的程式碼,修改後的Student.cs內容如下(注意名稱空間的變化)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMDemo.Model
{
public class Student
{
string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
}
}
string lastName;
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
}
}
public Student()
{
//模擬獲取資料
//這裡為什麼會有模擬資料一說呢?我是這樣認為的,有時候類的屬性會存在資料庫或者本地檔案系統等上面,
//我們需要讀取操作將這些資料載入到咱們定義的類裡。
Mock();
}
public void Mock()
{
FirstName = "firstName:" + DateTime.Now.ToString();
LastName = "lastName:" + DateTime.Now.ToString();
}
}
}
此時的檔案工程結構變為下圖
2.ViewModel部分
接著,在ViewModel資料夾右擊新增一個StudentViewModel類,內容如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MVVMDemo.Model;
namespace MVVMDemo.ViewModel
{
public class StudentViewModel:NotificationObject
{
private Student student;
public Student Student
{
get
{
return this.student;
}
set
{
this.student = value;
//下面這一句話的用法以後再拿出一章具體介紹
this.RaisePropertyChanged(() => this.student);
}
}
public StudentViewModel()
{
student = new Student();
}
}
}
此時檔案工程結構為下圖
3.View部分
再接著在View資料夾下新增一個使用者控制元件,命名為StudentView,它的XAML程式碼為下
<UserControl x:Class="MVVMDemo.View.StudentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="gridLayout">
<Grid.DataContext>
<vm:StudentViewModel />
</Grid.DataContext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
</Grid.RowDefinitions>
<TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Path=Student.FirstName,Mode=Default}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox Text="{Binding Path=Student.LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<Button x:Name="BtnView" Content="I am View" Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</Grid>
</UserControl>
注意,此時的XAML程式碼繫結有些變化,繫結的是Student.FirstName和Student.LastName,而不是FirstName和LastName。
此時檔案工程結構圖為下圖
然後在MainWindow裡需要引用這個控制元件,修改MainWindow.xaml的程式碼,內容如下
<Window x:Class="MVVMDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:MVVMDemo.View"
Title="MainWindow" Height="350" Width="525">
<Grid >
<view:StudentView />
</Grid>
</Window>
再將MainWindow.cs裡之前新增的程式碼刪掉,修改後的內容如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMDemo
{
/// <summary>
/// MainWindow.xaml 的互動邏輯
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
編譯工程,執行,效果如圖
該部分的程式碼,點此下載
4.若干解釋
在上述的工程Demo中,領域模型Student負責獲取資料,而資料來源於何處不是我們關心的重點(可能是資料庫,也可能是配置檔案,等等),所以,我們直接在Student中模擬了獲取資料的過程,即Mock方法。這相當於完成了一次OneWay的過程,即把後臺資料推送到前臺進行顯示,這隻能算是完成跟UI互動的一部分功能。UI互動還需要包括從UI中將資料持久化(如儲存到資料庫)。而UI跟後臺的互動,就需要通過命令繫結的機制去實現了。
5.命令繫結
在接下來的工程裡,我們演示兩類命令,一類是屬性類命令繫結,一類是事件類命令繫結 。
首先,我們知道,VM負責UI和領域模型的聯絡,所以,繫結所支援的方法一定是在VM中,於是,我們在StudentViewModel中定義一個屬性CanSubmit,及一個方法Submit
public bool CanSubmit
{
get
{
return true;
}
}
public void Submit()
{
student.Mock();
}
此時StudentViewModel的內容如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MVVMDemo.Model;
namespace MVVMDemo.ViewModel
{
public class StudentViewModel:NotificationObject
{
private Student student;
public Student Student
{
get
{
return this.student;
}
set
{
this.student = value;
//下面這一句話的用法以後再拿出一章具體介紹
this.RaisePropertyChanged(() => this.student);
}
}
public StudentViewModel()
{
student = new Student();
}
public bool CanSubmit
{
get
{
return true;
}
}
public void Submit()
{
student.Mock();
}
}
}
注意,上述Submit方法中為了簡單起見,使用了模擬方法。由於Mock方法中仍然可能涉及到UI的變動(如隨資料庫的某些具體的值變動而變動),故領域模型Student可能也會需要繼承NotificationObject,在本例中,Student改變如下
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMDemo.Model
{
public class Student : NotificationObject
{
string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
this.RaisePropertyChanged("FirstName");
}
}
string lastName;
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
this.RaisePropertyChanged("LastName");
}
}
public Student()
{
//模擬獲取資料
//這裡為什麼會有模擬資料一說呢?我是這樣認為的,有時候類的屬性會存在資料庫或者本地檔案系統等上面,
//我們需要讀取操作將這些資料載入到咱們定義的類裡。
Mock();
}
public void Mock()
{
FirstName = "firstName:" + DateTime.Now.ToString();
LastName = "lastName:" + DateTime.Now.ToString();
}
}
}
其次,需要改變StudentView,由於該VIEW用到命令和屬性繫結,所以需要新增兩個引用
新增完上述兩個引用後,修改StudentView.xaml的內容如下:
<UserControl x:Class="MVVMDemo.View.StudentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:vm="clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="gridLayout">
<Grid.DataContext>
<vm:StudentViewModel />
</Grid.DataContext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
<RowDefinition Height="5*" />
</Grid.RowDefinitions>
<TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Path=Student.FirstName,Mode=Default}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox Text="{Binding Path=Student.LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
<Button x:Name="BtnView" Content="I am View" IsEnabled="{Binding CanSubmit}" Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="Submit"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</UserControl>
編譯執行,點選按鈕BtnView,可以看到現實內容更新。
上述工程程式碼,點此下載
6.後言
經過這一次的重構之後,基本滿足了一個簡單的MVVM模型的需要,我也對MVVM大概有了認識,但是學習的過程中還設計到一些問題,我需要繼續探究,比如類NotificationObject裡的Lambda表示式,還有命令繫結。本學習筆記系列還沒有結束,一步一步來吧。