WPF 的 MVVM 測試
甚麼是WPF
WPF(Windows Presentation Foundation) 簡單的說是一種把UI跟程式碼拆開來做的windows應用程式,早期UI的設計與動作都會和程式碼扯上關聯(因為程式碼寫在UI裡面),導致做美編的人員要動到畫面設計時,還得小心考慮到每個物件裡面的程式碼。而微軟在2006年的.NET Framework 3.0以後,重新去定義Windows APP設計的方向與概念而訂製了 WPF 架構。
我想有在設計android程式的人大概見怪不怪了,畢竟android程式的設計就是如此的,UI跟程式碼本來就是分開在設計的。當然習慣於舊式Coding的程式設計師還是會有點難以適應的,因為它不是『直覺』去設計程式的,而是透過『分析』→『模組化』→『開發設計』這種方式去進行開發的。
雖然WPF也可以用比較傳統方式去開發(把各種程式碼寫在UI的片段下)。但由於『模組化』才是WPF的精隨,所以一開始就栽進 M-V-VM 的環境,對於將來開發會比較有幫助的。
網路上也有不少資料可以參考,我這邊就以我自己的想法去解釋,如一開始的圖片。
- Model: 主要負責資料結構(Data Model:Data Structure)的建立或與資料庫連結的存取(DAL;Data Access Layer)的各類模組化都是由這一層負責處理。
- View:主要負責畫面的設計與成型,它是一個 .xaml 的 XML 描述檔案,這個描述檔案描述了畫面個元件如何擺放與屬性等,通常它會搭配一個 .cs 的程式碼檔案,雖然可以把程式碼寫在 .cs 裡面,但為了把運算邏輯分離,所以盡可能不要寫入任何與畫面無關的程式碼,甚至根輸入有關的互動運算邏輯也不應該寫在這裡。
- ViewModel:這個才是View的邏輯核心,通常一個View會搭配一個ViewModel,而這個ViewModel負責 View 的資料保存(輸入或是顯示用)與畫面元件的互動或是指令,例如按鈕的行為、輸入資料後的自動動作等等,都是在這個模組去完成。透過 View 與 ViewModel 進行結繫(Biding)的方式,也可以讓 View 接收 ViewModel 傳回來的事件要求。
基本型:
假如我需要一個可以輸入產品名稱、數量、成本 與計算小計的功能。
所以我建立一個WPF專案,名稱叫 metoolook。
一、首先,定義一個資料類別(資料結構) ProductModel.cs
namespace metoolook.Models
{
public class ProductModel
{
public string Name { get; set; }
public int Amount { get; set; }
public int Cost { get; set; }
public int Total { get; set; }
}
}
這裡要注意的是我把這個類別放在 Models 資料夾下面,所以它的 namespace 是 metoolook.Models 。
所以接下來,所有使用到這個 Class 的程式碼都必須使用
using metoolook.Models;
二、再來開啟主畫面 MainWindows.xaml ,並設計一個 4個格位的格窗與輸入TextBox 和一個計算用的按鈕。
XAML 內容部分(只有 Grid 部分):
<Grid Margin="0,0,0,439">
<Grid x:Name="Grid1" HorizontalAlignment="Left" Height="108" Margin="56,69,0,-148" VerticalAlignment="Top" Width="641">
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Text="產品" Grid.Row="0" Grid.Column="0" TextAlignment="Center" FontSize="24" Background="Aqua"></TextBlock>
<TextBlock Text="數量" Grid.Row="0" Grid.Column="1" TextAlignment="Center" FontSize="24" Background="Aqua"></TextBlock>
<TextBlock Text="單價" Grid.Row="0" Grid.Column="2" TextAlignment="Center" FontSize="24" Background="Aqua"></TextBlock>
<TextBlock Text="小計" Grid.Row="0" Grid.Column="3" TextAlignment="Center" FontSize="24" Background="Aqua"></TextBlock>
<TextBox Text="" Grid.Row="1" Grid.Column="0" FontSize="20"></TextBox>
<TextBox Text="" Grid.Row="1" Grid.Column="1" TextAlignment="Right" FontSize="20"></TextBox>
<TextBox Text="" Grid.Row="1" Grid.Column="2" TextAlignment="Right" FontSize="20"></TextBox>
<TextBox Text="" Grid.Row="1" Grid.Column="3" TextAlignment="Right" FontSize="20"></TextBox>
</Grid>
<Button x:Name="button" Content="計算" HorizontalAlignment="Left" Height="30" Margin="702,93,0,-123" VerticalAlignment="Top" Width="75"/>
</Grid>
此時,這個 View 還沒有 ViewModel 可以結繫(Biding),接下來我們就要做一個 ViewModel 來給這個 View 使用。
三、設計 ViewModel ,設計這個 ViewModel 前要先瞭解二個介面 :
- INotifyPropertyChanged
- ICommand
INotifyPropertyChanged 這個介面是用來通知結繫端物件有屬性內容發生變更的,當結繫端收到此通知便會進行畫面更新的動作。
既然,每個 ViewModel 基本上都會用到 INotifyPropertyChanged ,所以就乾脆寫一個基本類別,然後再給 各別 ViewModel 繼承,這樣就不用每個 ViewModel 再寫一次 INotifyPropertyChanged 的實做功能了。
實做 INotifyPropertyChanged 基底 ViewModel 類別 :ViewModelBase.cs
(注意這個 Class 放在 ViewModels 資料夾下,所以 namespace 會不一樣)
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace metoolook.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(
[CallerMemberName] string PropertyName = null)
{
if (PropertyName != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(PropertyName));
}
}
}
}
這個裡面有個 [CallerMemberName] ,這個用法是會帶入呼叫端的名稱(可以是Property、Member、Function等的名稱 ),例如下面的呼叫範例:
public void FunctionA()
{
//呼叫FunctionB
FunctionB("MyApp");
FunctionB();
}
public void FunctionB([CallerMemberName] string PropertyName = null)
{
Console.WriteLine(PropertyName)
}
## 得到的結果:
MyApp ← 這是第一次的結果
FunctionA ←這是第二次的結果(當沒有參數時會帶入[CallerMemberName])
FunctionA ←這是第二次的結果(當沒有參數時會帶入[CallerMemberName])
認識一下 ICommand 介面,這個介面是用來接收 XAML(也就是View) 結繫的觸發使用,通常會用在 Button 物件上,它跟 INotifyPropertyChanged 不一樣的是,它沒辦法預先實做在基底(Base),由於它必須實做在對應屬性上,而每個按鈕可能會對應到不同屬性。
假如,你的 View 上面有 3個按鈕分別是 ButtonAdd、ButtonDelete、ButtonUpdate,那這樣你的 ViewModel 上就可能會做出 3種屬性對應
public ICommand AddData {get;}
public ICommand DeleteData {get;}
public ICommand UpdateData {get;}
但是,很奇怪那個屬性到底要回傳(get)甚麼呢,它其實是 回傳一個 具有 ICommand 介面樣式的物件,而這個物件要能夠接受並處理 ICommand 要求的 Action 和 Func<bool> 兩個參數,
- Action:指定要執行的 Function 或是 Method,其時就像 Callback動作。
- Func<bool>:指定要執行的 CallBack 並一定要回傳一個 bool 值,用來判斷 Action 是否可以執行,感覺就像Enabled/Disabled。
所以上面的 get 內容我門就要去實做一個 ICommand 類別讓它回傳,最標準的 ICommand 類是網路常見的 RelayCommand.cs
using System;
using System.Diagnostics;
using System.Windows.Input;
namespace metoolook
{
public class RelayCommand :ICommand
{
readonly Func<bool> _canExecute;
readonly Action _execute;
// 建構子(多型)
public RelayCommand(Action execute) :
this(execute, null)
{
}
// 建構子(傳入參數)
public RelayCommand(Action execute, Func<bool> canExecute)
{
// 簡化寫法 if(execute == null) throw new ArgumentNullException("execute");
_execute = execute ?? throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
// 當_canExecute發生變更時,加入或是移除Action觸發事件
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null) CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null) CommandManager.RequerySuggested += value;
}
}
// 下面兩個方法是提供給 View 使用的
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute();
}
public void Execute(object parameter)
{
_execute();
}
}
}
※這個標準的 RelayCommand 是不具備接收參數能力的,如果要接收參數就要做成 DelegateCommand 泛型版本,可以參考這裡。
完成 之後,上面3個按鈕範例就會改成如下:
public ICommand AddData {get{return new RelayCommand(ToAdd , CanDo); }}
public ICommand DeleteData {get{return new RelayCommand(ToDelete , CanDo); }}
public ICommand UpdateData {get{return new RelayCommand(ToUpdate , CanDo); }}
public void ToAdd()
{
// 新增資料動作...
}
public void ToDelete()
{
// 刪除資料動作...
}
public void ToUpdate()
{
// 修改資料動作...
}
public bool CanDo()
{
// 檢查是否enabled...
return true;
}
好了,有了上面幾項東西後,
接下來開始設計我們需要的 ViewModel : ProductViewModel.cs
using metoolook.Models;
using System.Windows.Input;
namespace metoolook.ViewModels
{
public class ProductViewModel : ViewModelBase
{
// 使用的資料來源
private ProductModel product;
// 建構子
public ProductViewModel()
{
// 建立資料模型 (這裡沒有對資料初始化)
product = new ProductModel();
}
// 對應屬性:ICommand 類型,提供給 View 上面的 button 使用,所以沒有 set
public ICommand UpdateTotal
{
get { return new RelayCommand(CalcTotal, CanExecute); }
}
// 對應屬性:輸入欄位:產品
public string ProductName
{
get { return product.Name; }
set
{
product.Name = value;
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:數量
public int ProductAmount
{
get { return product.Amount; }
set
{
product.Amount = value;
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:單價
public int ProductCost
{
get { return product.Cost; }
set
{
product.Cost = value;
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:小計
public int ProductTotal
{
get { return product.Total; }
set
{
product.Total = value;
OnPropertyChanged();
}
}
// 計算小計(給UpdateTotal使用)
public void CalcTotal()
{
ProductTotal = ProductCost * ProductAmount;
}
// 回應是否可執行(給UpdateTotal使用)
public bool CanExecute()
{
return true;
}
}
}
所以你看,ViewModel 上的屬性欄位都會與 xaml 相關的屬性來相應,到這裡只是初步完成屬性建置,但是 View 和 ViewModel 還沒有做結繫(Biding) ,因此View 上面的任何動作都不會與 ViewModel 產生互動,因此接下來,我們準備做結繫。
四、建置專案
接下來就要做 Biding 動作了,在此之前一定要做一次建置,不然的話,在 Biding 時,設計界面會一直告訴你找不到對應的 ViewModel 喔!!!
五、結繫(Biding)
回到我們的 MainWindows.xaml 裡面,先設定 DataContext,這裡要注意的一點是我們的 ProductViewModel.cs 是放在 metoolook.ViewModels 下面,所以必須把 namespace 先加進來。
然後,我們再把 TextBox 和 Button 去和 ProductViewModel 的屬性去做個結繫
六、執行
按下計算
進階型:
如果我們希望讓它能夠自動計算,而不需要按『計算』按鈕,那要如何做呢?
- 把自動計算放在 Model 裡面,雖然可以做,其實個人不太建議,因為按照邏輯層此處不應該做邏輯處理,而且也沒辦法儲存其值。但是為了瞭解其運作背景,所以還是把它實做一次。
- 把自動計算放在 ViewModel裡面,這樣只需專注於 View 和 ViewModel 之間的關係處理。
一、把自動計算放在 Model 裡面的話就是改 Model 的 Total 屬性讓它自動計算
修改 ProductModel.cs
namespace metoolook.Models
{
public class ProductModel
{
public string Name { get; set; }
public int Amount { get; set; }
public int Cost { get; set; }
public int Total { get { return Amount * Cost; } }
}
}
修改 ProductViewModel.cs:
using metoolook.Models;
using System.Windows.Input;
namespace metoolook.ViewModels
{
public class ProductViewModel : ViewModelBase
{
// 使用的資料來源
private ProductModel product;
// 建構子
public ProductViewModel()
{
// 建立資料模型 (這裡沒有對資料初始化)
product = new ProductModel();
}
// 對應屬性:輸入欄位:產品
public string ProductName
{
get { return product.Name; }
set
{
product.Name = value;
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:數量
public int ProductAmount
{
get { return product.Amount; }
set
{
product.Amount = value;
OnPropertyChanged("ProductTotal");
}
}
// 對應屬性:輸入欄位:單價
public int ProductCost
{
get { return product.Cost; }
set
{
product.Cost = value;
OnPropertyChanged("ProductTotal");
}
}
// 對應屬性:輸入欄位:小計
public int ProductTotal
{
get { return product.Total; }
}
}
}
由於不需要 button 動作,所以把 ICommand UpdateTotal 、 CalcTotal、CanExecute 屬性拿掉,並修改 ProductTotal 只能 get (對照 ProductModel 屬性)。
接下來很重要的,是當你輸入數量、單價後要觸發 ProductTotal 去更新畫面欄位,所以原本的
OnPropertyChanged();
要改成
OnPropertyChanged("ProductTotal");
強迫小計欄位去做 get 動作,當觸發 get 動作,就可以觸發 Total 裡面的計算公式。
※這裡有個盲點,若當資料來自 Model 時就會發生數量、單價這兩個欄位不會發生更新狀況了,除非在數量、單價屬性再做一次 OnPropertyChanged()。
然後,既然 ProductTotal 被改成唯獨狀態,所以 xaml 裡面那個結繫 ProductTotal 的 TextBox 就必須改成唯讀的 TextBlock 喔!!!
測試結果
二、把自動計算放在 ViewModel裡面(UI邏輯層)
維持 ProductModel.cs 原本樣式:
namespace metoolook.Models
{
public class ProductModel
{
public string Name { get; set; }
public int Amount { get; set; }
public int Cost { get; set; }
public int Total { get; set; }
}
}
然後 ProductViewModel.cs 裡面在 ProductAmount、ProductCost 變更資料時去呼叫 CalcTotal 就可以了:
using metoolook.Models;
using System.Windows.Input;
namespace metoolook.ViewModels
{
public class ProductViewModel : ViewModelBase
{
// 使用的資料來源
private ProductModel product;
// 建構子
public ProductViewModel()
{
// 建立資料模型 (這裡沒有對資料初始化)
product = new ProductModel();
}
// 對應屬性:輸入欄位:產品
public string ProductName
{
get { return product.Name; }
set
{
product.Name = value;
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:數量
public int ProductAmount
{
get { return product.Amount; }
set
{
product.Amount = value;
CalcTotal();
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:單價
public int ProductCost
{
get { return product.Cost; }
set
{
product.Cost = value;
CalcTotal();
OnPropertyChanged();
}
}
// 對應屬性:輸入欄位:小計
public int ProductTotal
{
get { return product.Total; }
set
{
product.Total = value;
OnPropertyChanged();
}
}
// 計算小計(給UpdateTotal使用)
public void CalcTotal()
{
ProductTotal = ProductCost * ProductAmount;
}
}
}
測試結果:
結果是一樣的,只是關於以後的維護,還是分門別類放清楚比較好。
參考:
一篇关于C# WPF MVVM 实战与总结
天空的垃圾場 -WPF – MVVM (一)
天空的垃圾場 -WPF – MVVM (二)
天空的垃圾場 -WPF – MVVM (三)
高級打字工!!! -WPF MVVM 實作
程式學習紀錄 -WPF MVVM架構
實現Icommand介面- 掃文資訊
Marcus的奇幻旅程 -[WPF]MVVM開發架構(二)
Marcus的奇幻旅程 -[WPF]實作DelegateCommand
留言
public ICommand DeleteData {get;}
public ICommand UpdateData {get;}
請問以上是被宣告在哪個cs裡面呢?
這個沒有cs喔..因為這是個概念描述,
在我這篇文章範例中只使用一個『按鈕』,就是『計算』這個功能,
這個『計算』就是對應到文中 ProductViewModel.cs 內的 public ICommand UpdateTotal,其實你把它當作 public ICommand UpdateData {get;} 方法來看就是了