C# WPF MVVM 实战 - 2.1
上一篇,只介绍 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
- // 待续 ...
- }
- }
效果图: