上一篇介紹了增加刪除行可以怎樣做,現在說填寫時候,在某一欄讓用戶選擇,選項的集合是每行數據共用。想說說一個真的系統內,你或許要的一些設計、一些你需要做的決定。
技術上,這次有 :
- BackgroundWorker 加載列表
- 消除采購訂單 ViewModel 對另一個它自己要打開的 View 的依賴
- 要 Routed Event 的地方你要綁個 ICommand 過去的辦法
DataGrid 明細行內選擇物料,物料列表是集合,但集合不在明細行的類內
接上篇,物料列表是明細行多行共用一個集合。了解綁定寫法的,或許第一想到的就是 RelativeSource 用 FindAncestor 模式,DataGrid 單行內某欄的控件ComboBox 的 ItemSource 也能綁過去 VM 頂層的集合。嗯,你只有三四個選項那好辦,但這是物料號,舉例我有 50,000 個物料,用 ComboBox 的用戶體驗不好。這數量的選項,你有很多選擇可以用的,比如用可編輯的 ComboBox 然后做自動過濾選項加上寫 ComboxBox 拉下的模板,又比如彈出子窗體讓用戶篩選、選擇。
我用子窗體示范。
何時加載物料列表
我會把同步讀取放在 VM 構造函數啟動來作示例。因為,我認為填新采購訂單,用戶是先填表頭再填明細行,我要的是在用戶打開填寫時候,甚至是界面還沒出現前(VM 構造函數運行在 View.Show() 之前),就開始背景加載物料列表。這做法,在 VM 加載,意味着要重新打開采購訂單界面,才能刷新物料列表。
設計
我這做法很簡單,同一個 ViewModel 綁兩個 View。原來的采購訂單是一個 View,彈出窗口是另一個 View。大家的 DataContext 是同一個 ViewModel 這樣會少了很多麻煩,后果是 ViewModel 代碼變長。我覺得,就一個選擇用的窗體而已,不想分別寫 VM。如果你要分開,請注意,在采購訂單的 ViewModel 分線程加載后更新的集合,你需要有辦法通知子窗體的 ViewModel 讓它更新視圖。
采購訂單 ViewModel 加入物料集合及讀取線程
首先是采購訂單的 ViewModel 內,初始化時開線程讀取列表。例子中用 BackgroundWorker,用它比較省事。
讀數據跟 MVVM 關系不大。留意一下 InventoryListWorker.RunWorkerCompleted,我把結果放進去兩個 List<T>,子窗體過濾時候用,一個是全的、代碼不動它的,一個是用戶過濾后的。這樣比較省事。
采購訂單 ViewModel 加入打開子窗體的命令
代碼邏輯很簡單,Modal Window 用 ShowDialog() 打開,連查看 ShowDialog() 回傳的 true/false 都省掉了,因為是同一個 VM,子窗體直接操作采購訂單 VM 內的明細行,下部分講。
或許看到這里,new 關鍵字,子窗體類(View)出現在 VM 代碼,馬上眉頭皺。這不是 MVVM 哦。這里 VM 對 View 依賴。
一下重構就把它滅了。要把子窗體分離出來,想不 new 它,你可以寫視圖服務讓它提供,工廠之類,有 IoC 容器或許更方便。要做不做,你在不在乎這依賴,請自行判斷。
然后,像這示例沒 IoC 容器的,只能在外面 new 咯。
這樣做,你不需要任何 View,就可以測試這 MainWindowViewModel 類。測試下一篇講。
子窗體的屬性與命令
首先看看這子窗體的外貌:
兩個輸入框用來拿過濾用(綁定兩個 string 屬性),兩個按鈕(綁定兩個 ICommand),一個 DataGrid (綁定過濾后的集合)。還有一個,DataGrid 內 Double Click 命令綁定,讓用戶雙擊選物料。
對 MVVM 開始理解了的話,下面這些就很簡單。我過濾沒有什么算法,直接 LINQ,然后替換集合而已。唯一麻煩的是,Double Click 的綁定。
子窗體的 DataGrid 雙擊綁定
關於窗體,我唯一想說的,是雙擊。
首先,微軟有病。控件的命令,為何有時候是指定一定要 RoutedCommand,有時候又可以 ICommand。我不知道,問他們不要問我。
要解決,閑着沒事自己寫 Attached Property + 轉換,或者,下個源碼,測試過你喜歡的,或許改一下,收錄進去代碼庫。你會經常用到的。
我拿個 BehaviorBinding 的代碼來改。原版它本身有個問題,就是你綁定沒寫或者名字錯,綁不到,它會拋異常。這與其他屬性綁定的行為不一樣。你覺得沒什么大不了的話,直接用。讓它吃掉異常這很好改,我這部分不公開了。原版的源碼好像是這里的,我不太記得,打不開請翻牆,或者自行 Google。
我做法,兩步,第一是 Preview 讓它執行 ICommand,第二是這個 View 的 DataGrid_MouseDoubleClick。第二步是干嘛?沒干嘛:
重要部分的源碼
講代碼部分,終於結束。其實代碼挺簡單的,幾百行其實大部分都是屬性的 get/set,說那么多其實是想讓大家在決定是否用 MVVM 時候心里有個底,也希望大家有個概念怎樣搞。下篇才講測試。低耦合人人愛之余,我認為測試也是 MVVM 的重點所在。
app.xaml.cs
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
Views.Window1 view = new Views.Window1();
ViewModels.MainWindowViewModel vm =
new ViewModels.MainWindowViewModel( new Views.ViewProvider());
view.DataContext = vm;
view.Show();
}
}
}
Window1.xaml
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my ="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title ="WPF MVVM 實戰 - 新添加采購訂單" Height ="500" Width ="668.772"
>
< DockPanel >
<!-- 單據表頭部分 -->
< Grid DockPanel.Dock ="Top" Height ="200" >
< ComboBox Height ="23" Margin ="121.465,21.435,160.048,0"
VerticalAlignment ="Top"
ItemsSource =" {Binding SupplierList} "
DisplayMemberPath ="Name"
SelectedItem =" {Binding SelectedSupplier} "
/>
< my:DatePicker Margin ="121.465,58.069,160.048,0" Height ="24.233"
VerticalAlignment ="Top"
SelectedDate =" {Binding DocDate} " />
< TextBox Margin ="121.465,88.598,160.048,88.598"
Text =" {Binding DocNo} " />
< Label HorizontalAlignment ="Left" Margin ="6,21.435,0,0"
Width ="100.03" Height ="28"
VerticalAlignment ="Top" >供應商 </ Label >
< Label Height ="28" HorizontalAlignment ="Left"
Margin ="6.577,54.302,0,0" VerticalAlignment ="Top"
Width ="100.03" >單據日期 </ Label >
< Label HorizontalAlignment ="Left" Margin ="6.577,88.598,0,82.882"
Width ="100.03" >單據號 </ Label >
< Label Height ="28.52" HorizontalAlignment ="Left"
Margin ="6.577,0,0,48.586" VerticalAlignment ="Bottom"
Width ="100.03" >備注 </ Label >
< TextBox Height ="58.589" Margin ="121.465,0,160.048,18.577"
VerticalAlignment ="Bottom" TextWrapping ="Wrap"
Text =" {Binding DocRemark} " />
< TextBlock Height ="21" HorizontalAlignment ="Right"
Margin ="0,23.435,6,0" Name ="textBlock1"
VerticalAlignment ="Top" Width ="146.903"
Text =" {Binding SelectedSupplier.ContactPerson} " />
</ Grid >
<!-- 單據操作按鈕部分 -->
< Grid DockPanel.Dock ="Bottom" Height ="50" >
< Button HorizontalAlignment ="Right" Margin ="0,19.722,6,6"
Width ="75" Content ="取消"
Command =" {Binding CloseViewCommand} " Click ="Button_Click" />
< Button HorizontalAlignment ="Right" Margin ="0,19.722,87.169,6"
Width ="75" Content ="保存"
Command =" {Binding SaveCommand} " />
</ Grid >
<!-- 單據表體,明細行部分 -->
< DockPanel >
<!-- 添加刪除行按鈕 -->
< StackPanel Orientation ="Horizontal" DockPanel.Dock ="Top" Height ="30" >
< Button Content ="添加行" Margin ="3,3,3,3"
Command =" {Binding AddRowCommand} " />
< Button Content ="刪除行" Margin ="3,3,3,3"
Command =" {Binding DeleteRowCommand} " />
</ StackPanel >
<!-- 明細行表格 -->
< my:DataGrid CanUserAddRows ="False"
AutoGenerateColumns ="False"
ItemsSource =" {Binding purchaseOrder.PoDetails} "
SelectedItem =" {Binding CurrentRow} "
Name ="myDataGrid" >
< my:DataGrid.Resources >
< SolidColorBrush x:Key =" {x:Static SystemColors.HighlightBrushKey} "
Color ="LightBlue" />
</ my:DataGrid.Resources >
< my:DataGrid.Columns >
< my:DataGridTemplateColumn Header ="物料號" >
< my:DataGridTemplateColumn.CellEditingTemplate >
< DataTemplate >
< StackPanel Orientation ="Horizontal" >
< TextBlock MinWidth ="100" Text =" {Binding ItemCode} " />
< Button Content ="..."
Command =" {Binding DataContext.ItemCodeSelectionCommand,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}} " />
</ StackPanel >
</ DataTemplate >
</ my:DataGridTemplateColumn.CellEditingTemplate >
< my:DataGridTemplateColumn.CellTemplate >
< DataTemplate >
< TextBlock MinWidth ="100" Text =" {Binding ItemCode} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellTemplate >
</ my:DataGridTemplateColumn >
< my:DataGridTextColumn Header ="數量" Binding =" {Binding OrderedQty} " />
< my:DataGridTemplateColumn Header ="要求交貨日期" >
< my:DataGridTemplateColumn.CellEditingTemplate >
< DataTemplate >
< my:DatePicker SelectedDate =" {Binding RequestedDeliveryDate} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellEditingTemplate >
< my:DataGridTemplateColumn.CellTemplate >
< DataTemplate >
< TextBlock Text =" {Binding RequestedDeliveryDate, StringFormat=dd/MM/yyyy} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellTemplate >
</ my:DataGridTemplateColumn >
< my:DataGridTextColumn Header ="備注" Width ="200"
Binding =" {Binding Remark} " />
</ my:DataGrid.Columns >
</ my:DataGrid >
</ DockPanel >
</ DockPanel >
</ Window >
MainWindowViewModel.cs
public class MainWindowViewModel : ViewModelBase {
private BackgroundWorker InventoryListWorker;
private BackgroundWorker SupplierListWorker;
private readonly Views.IViewProvider viewProvider;
public MainWindowViewModel(Views.IViewProvider viewProvider) {
this.viewProvider = viewProvider;
Initialize();
InitializeAndStartWorkers();
}
private void Initialize() {
// 新的采購訂單業務對象
purchaseOrder = new Models.PurchaseOrder();
purchaseOrder.PoDetails = new ObservableCollection<Models.PurchaseOrderDetail>();
}
private void InitializeAndStartWorkers() {
// 讀取供應商列表
SupplierListWorker = new BackgroundWorker();
SupplierListWorker.DoWork += (s, e) => {
e.Result = DataAccess.DataProvider.GetAllSuppliers();
};
SupplierListWorker.RunWorkerCompleted += (s, e) => {
if (!(e.Error == null)) {
System.Windows.MessageBox.Show( " 獲取供應商數據失敗: " + e.Error.Message);
} else {
this.SupplierList = (List<Models.Supplier>)e.Result;
}
};
SupplierListWorker.RunWorkerAsync();
// 讀取物料列表
InventoryListWorker = new BackgroundWorker();
InventoryListWorker.DoWork += (s, e) => {
e.Result = DataAccess.DataProvider.GetAllInventoryItems();
};
InventoryListWorker.RunWorkerCompleted += (s, e) => {
if (!(e.Error == null)) {
System.Windows.MessageBox.Show( " 獲取物料列表數據失敗: " + e.Error.Message);
} else {
this.ItemList = (List<Models.Inventory>)e.Result;
this.FilteredItemList = this.ItemList;
}
};
InventoryListWorker.RunWorkerAsync();
}
#region Acutal Model Object reference
public Models.PurchaseOrder purchaseOrder {
get;
set;
}
#endregion
#region Supplier Selection Combo Box
private Models.Supplier selectedSupplier;
public Models.Supplier SelectedSupplier {
get {
return selectedSupplier;
}
set {
if (selectedSupplier != value) {
selectedSupplier = value;
purchaseOrder.SupplierCode = value.SupplierCode;
OnPropertyChanged( " SelectedSupplier ");
}
}
}
private List<Models.Supplier> supplierList;
public List<Models.Supplier> SupplierList {
get {
return supplierList;
}
set {
supplierList = value;
OnPropertyChanged( " SupplierList ");
}
}
#endregion
#region Purchase Order Document Number, Date, Remark
public string DocNo {
get {
return purchaseOrder.DocNo;
}
set {
purchaseOrder.DocNo = value;
OnPropertyChanged( " DocNo ");
}
}
public DateTime DocDate {
get {
return purchaseOrder.DocDate;
}
set {
purchaseOrder.DocDate = value;
OnPropertyChanged( " DocDate ");
}
}
public string DocRemark {
get {
return purchaseOrder.Remark;
}
set {
purchaseOrder.Remark = value;
OnPropertyChanged( " DocRemark ");
}
}
#endregion
#region Detail Rows Add and Remove Commands
private Models.PurchaseOrderDetail currentRow;
public Models.PurchaseOrderDetail CurrentRow {
get {
return currentRow;
}
set {
if (currentRow != value) {
currentRow = value;
OnPropertyChanged( " CurrentRow ");
}
}
}
RelayCommand addRowCommand;
public ICommand AddRowCommand {
get {
if (addRowCommand == null) {
addRowCommand = new RelayCommand(x => this.AddRow());
}
return addRowCommand;
}
}
private void AddRow() {
this.purchaseOrder.PoDetails.Add( new Models.PurchaseOrderDetail());
}
RelayCommand deleteRowCommand;
public ICommand DeleteRowCommand {
get {
if (deleteRowCommand == null) {
deleteRowCommand = new RelayCommand(
x => this.DeleteRow(),
x => {
return this.CurrentRow != null;
}
);
}
return deleteRowCommand;
}
}
private void DeleteRow() {
this.purchaseOrder.PoDetails.Remove(CurrentRow);
CurrentRow = null;
}
#endregion
#region Popup window item list and filtered list
private List<Models.Inventory> itemList;
public List<Models.Inventory> ItemList {
get {
return itemList;
}
set {
itemList = value;
OnPropertyChanged( " ItemList ");
}
}
private List<Models.Inventory> filteredItemList;
public List<Models.Inventory> FilteredItemList {
get {
return filteredItemList;
}
set {
filteredItemList = value;
OnPropertyChanged( " FilteredItemList ");
}
}
#endregion
#region Command for open popup window, note the tight coupling here
RelayCommand itemCodeSelectionCommand;
public ICommand ItemCodeSelectionCommand {
get {
if (itemCodeSelectionCommand == null) {
itemCodeSelectionCommand =
new RelayCommand(x => this.OpenItemSelectionDialog());
}
return itemCodeSelectionCommand;
}
}
private void OpenItemSelectionDialog() {
System.Windows.Window dialog = viewProvider.GetItemCodeSelectionWindow();
dialog.DataContext = this;
dialog.ShowDialog();
}
#endregion
#region Popup window binding properties and commands for filter
private string searchItemNameText;
public string SearchItemNameText {
get {
return searchItemNameText;
}
set {
searchItemNameText = value;
OnPropertyChanged( " SearchItemNameText ");
}
}
private string searchItemSpecText;
public string SearchItemSpecText {
get {
return searchItemSpecText;
}
set {
searchItemSpecText = value;
OnPropertyChanged( " SearchItemSpecText ");
}
}
RelayCommand searchCommand;
public ICommand SearchCommand {
get {
if (searchCommand == null) {
searchCommand = new RelayCommand(x => this.Search());
}
return searchCommand;
}
}
private void Search() {
if (!String.IsNullOrEmpty(SearchItemNameText)) {
FilteredItemList = ItemList
.Where(x => x.ItemName.ToLower().Contains(SearchItemNameText.ToLower()))
.ToList();
}
if (!String.IsNullOrEmpty(SearchItemSpecText)) {
FilteredItemList = ItemList
.Where(x => x.Specification.ToLower().Contains(SearchItemSpecText.ToLower()))
.ToList();
}
}
RelayCommand clearResultCommand;
public ICommand ClearResultCommand {
get {
if (clearResultCommand == null) {
clearResultCommand = new RelayCommand(x => this.ClearResult());
}
return clearResultCommand;
}
}
private void ClearResult() {
FilteredItemList = ItemList;
SearchItemNameText = string.Empty;
SearchItemSpecText = string.Empty;
}
#endregion
#region Popup window double click DataGrid Command and property
private Models.Inventory selectedInventoryItem;
public Models.Inventory SelectedInventoryItem {
get {
return selectedInventoryItem;
}
set {
selectedInventoryItem = value;
OnPropertyChanged( " SelectedInventoryItem ");
}
}
RelayCommand selectCommand;
public ICommand SelectCommand {
get {
if (selectCommand == null) {
selectCommand = new RelayCommand(x => this.Select());
}
return selectCommand;
}
}
private void Select() {
if (SelectedInventoryItem != null) {
CurrentRow.ItemCode = SelectedInventoryItem.ItemCode;
}
}
#endregion
}
}
IViewProvider.cs
public interface IViewProvider {
System.Windows.Window GetItemCodeSelectionWindow();
}
}
ViewProvider.cs
public class ViewProvider :IViewProvider{
#region IViewProvider Members
public System.Windows.Window GetItemCodeSelectionWindow() {
return new ItemCodeSelectionWindow();
}
#endregion
}
}
ItemCodeSelectionWindow.xaml
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my ="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:cmd ="clr-namespace:IPE.Framework.UI.Commands;assembly=IPE.Framework"
Title ="請選擇物料" MinHeight ="300" MinWidth ="400"
WindowStyle ="ToolWindow" >
< DockPanel >
< Grid DockPanel.Dock ="Top" Height ="50" >
< Label HorizontalAlignment ="Left" Margin ="19,6,0,16" Name ="label2" Width ="59" >物料名: </ Label >
< TextBox Text =" {Binding SearchItemNameText} " Margin ="70,6,0,21" Name ="textBox1" HorizontalAlignment ="Left" Width ="120" />
< Label Margin ="205,6,250,16" Name ="label3" >規格: </ Label >
< TextBox Text =" {Binding SearchItemSpecText} " Margin ="245,6,137,21" Name ="textBox2" />
< Button Command =" {Binding SearchCommand} " HorizontalAlignment ="Right" Margin ="0,4.638,73,22.362" Width ="58" Content ="搜索" />
< Button Command =" {Binding ClearResultCommand} " HorizontalAlignment ="Right" Margin ="0,4.638,9,22.362" Width ="58" Content ="清空搜索" />
</ Grid >
< my:DataGrid AutoGenerateColumns ="False"
CanUserAddRows ="False"
ItemsSource =" {Binding FilteredItemList} "
SelectedItem =" {Binding SelectedInventoryItem} "
MouseDoubleClick ="DataGrid_MouseDoubleClick"
>
< cmd:CommandBehaviorCollection.Behaviors >
< cmd:BehaviorBinding Event ="PreviewMouseDoubleClick" Command =" {Binding SelectCommand} " />
</ cmd:CommandBehaviorCollection.Behaviors >
< my:DataGrid.Resources >
< SolidColorBrush x:Key =" {x:Static SystemColors.HighlightBrushKey} "
Color ="LightBlue" />
</ my:DataGrid.Resources >
< my:DataGrid.Columns >
< my:DataGridTemplateColumn Header ="物料號" MinWidth ="100" >
< my:DataGridTemplateColumn.CellTemplate >
< DataTemplate >
< TextBlock Text =" {Binding ItemCode} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellTemplate >
</ my:DataGridTemplateColumn >
< my:DataGridTemplateColumn Header ="物料名稱" MinWidth ="200" >
< my:DataGridTemplateColumn.CellTemplate >
< DataTemplate >
< TextBlock Text =" {Binding ItemName} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellTemplate >
</ my:DataGridTemplateColumn >
< my:DataGridTemplateColumn Header ="規格" MinWidth ="200" >
< my:DataGridTemplateColumn.CellTemplate >
< DataTemplate >
< TextBlock Text =" {Binding Specification} " />
</ DataTemplate >
</ my:DataGridTemplateColumn.CellTemplate >
</ my:DataGridTemplateColumn >
</ my:DataGrid.Columns >
</ my:DataGrid >
</ DockPanel >
</ Window >