上一篇,只介紹 VM 與 View 是如何關聯起來,說了些注意項,還有個超簡化的例子。這次來點比較實際的,比較靠近項目內會遇到的。
這次看看,采購訂單這業務單據,在 MVVM 模式中實現方式的一個演示。實現方式很多,這示范也只是其中一種。這內容比較多,要分開幾次講。
說在前面,以下是用 VS 2008,.net 3.5,以及對應的 WPF Toolkit 制作。這樣的話,應該絕大部分人都能應用以下例子。
MODELS
假設,系統是有供應商記錄,也有物料記錄,作為主數據。單據記錄就是采購訂單。整個業務層由這四個類組成。設計從 Model 做起,Model 來自用例,這比較自然。數據結構就這樣先吧:
代碼如下:
- namespace Lepton_Practical_MVVM_2.Models
- {
- public class Supplier
- {
- public int Id { get; set; }
- public string SupplierCode { get; set; }
- public string Name { get; set; }
- public string BillAddress { get; set; }
- public string ShipmentAddress { get; set; }
- public string ContactPerson { get; set; } // 聯系人
- }
- }
- namespace Lepton_Practical_MVVM_2.Models
- {
- public class Inventory
- {
- public int Id { get; set; }
- public string ItemCode { get; set; }
- public string ItemName { get; set; }
- public string Specification { get; set; }
- public string Uom { get; set; } // 計量單位
- }
- }
- using System;
- namespace Lepton_Practical_MVVM_2.Models
- {
- public class PurchaseOrderDetail
- {
- public int Id { get; set; }
- public int ParentId { get; set; }
- public string ItemCode { get; set; }
- public decimal OrderedQty { get; set; }
- public DateTime? RequestedDeliveryDate { get; set; } // 要求送貨日期
- public string Remark { get; set; }
- }
- }
- using System;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- namespace Lepton_Practical_MVVM_2.Models
- {
- public class PurchaseOrder
- {
- public int Id { get; set; }
- public string DocNo { get; set; }
- public DateTime DocDate { get; set; }
- public string Remark { get; set; }
- public string SupplierCode { get; set; }
- public IList<PurchaseOrderDetail> PoDetails { get; set; } // 行明細
- }
- }
采購訂單有表頭與明細行,兩個部分組成,明細行在 PurchaseOrder 類內是 IList<T> 因為一張單可以有多行記錄,一對多,我用 IList 因為准備用 NHibernate 做 ORM。你喜歡其他集合也可以。熟悉商用開發的朋友,應該對這樣的結構很熟悉了。其他我不多說了。
VIEWS
然后看看界面,是這樣樣子:
呃,有點丑。咱們還是看代碼吧…
- <Window x:Class="Lepton_Practical_MVVM_2.Views.Window1"
- 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}"/>
- <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}" >
- <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 ItemCodeSelectionCommand}"/>
- </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>
整個布局,用 DockPanel,分上中下三個部分,分別用來放置表頭,明細行,和操作按鈕。明細行區域又用了 DockPanel 再分開了添加行、刪除行按鈕區域,和明細行的 GridView。整個 XAML 我唯一調過樣式的,是 GridView 的當前行高亮底色,原來的藍色實在太刺眼了。
全部綁定都是寫 Path,因為整個 Window 的 DataContext 就是 ViewModel,它提供一切數據(或者負責指向實際業務類的實例)。我假設大家會用 Template,會一般的綁定,不解釋了。
VIEWMODELS
然后是 ViewModel,我從表頭開始講。
里面比較有趣的,是一個 Combo Box,它應該出現的選項,是 Supplier 業務類的集合。再看看 PurchaseOrder 這個類的結構,用戶選了 Supplier 之后,放進去 PurchaseOrder 不是 Supplier 實例,而是 SupplierCode 。
除此之外,看看 XAML ,我還搞了一個 TextBlock 在 Combo Box 旁邊,用來顯示一些關於這 Supplier 供應商的額外信息,比如我顯示了聯系人。
要做到這兩點需求,不能單靠 Path 綁來綁去就能解決,我需要一個已選擇了的供應商對象,存放在 ViewModel,然后在 TextBlock 綁過去,用 Path 指定要顯示信息的路徑。在我這例子,這已選定的供應商屬性,變量名是 SelectedSupplier,我要顯示聯系人,所以整個 Binding 的路徑就是 SelectedSupplier.ContactPerson,見 XAML 第 38 行。
我認為這做法的好處是,如果有哪天你需要更多關於該選定供應商的信息,顯示在界面,你 ViewModel 啥都不用改,只在 View 的 XAML 加個控件設一下路徑即可。
然后,選定的供應商,是這樣傳到 SelectedSupplier 屬性的:
那么,供應商編號,又是如何傳進去 Model (PurchaseOrder)的 SupplierCode 呢?就在 ViewModel 的 SelectedSupplier 中 Setter 代碼,這里:
每一次選定的供應商變化,由 Combo Box 綁定至 SelectedSupplier,而它除了更新屬性值以外,還同時更新到業務對象 PurchaseOrder 的實例屬性 SupplierCode 內。
整個 Combo Box 和它的“額外信息”,就是這樣處理了。
看看到目前為止的 ViewModel 代碼,數據層代碼我不貼出來了,我只是做了些假數據讓數據層提供而已。我代碼內的 FillSupplierList 方法,也應該開線程來讀,各位自己注意一下自己改吧。其他部分下次繼續…
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Collections.ObjectModel;
- using System.Windows.Input; // ICommand
- using IPE.Framework.UI.ViewModels; // ViewModelBase
- using IPE.Framework.UI.Commands; // RelayCommand
- namespace Lepton_Practical_MVVM_2.ViewModels
- {
- public class MainWindowViewModel : ViewModelBase
- {
- public MainWindowViewModel()
- {
- Initialize();
- }
- private void Initialize()
- {
- purchaseOrder = new Models.PurchaseOrder();
- purchaseOrder.PoDetails = new ObservableCollection<Models.PurchaseOrderDetail>();
- SupplierList = new ObservableCollection<Models.Supplier>();
- FillSupplierList();
- }
- private void FillSupplierList()
- {
- List<Models.Supplier> customerlist = DataAccess.DataProvider.GetAllCustomers();
- foreach (Models.Supplier customer in customerlist)
- {
- this.SupplierList.Add(customer);
- }
- customerlist = null;
- }
- #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");
- }
- }
- }
- public ObservableCollection<Models.Supplier> SupplierList { get; set; }
- #endregion
- // 待續 ...
- }
- }
效果圖: