Saturday, July 21, 2012

C# WPF MVVM 实战 - 2.1

原文发布在博客园:http://www.cnblogs.com/leptonation/archive/2012/05/09/2493545.html

上一篇,只介绍 VM 与 View 是如何关联起来,说了些注意项,还有个超简化的例子。这次来点比较实际的,比较靠近项目内会遇到的。

这次看看,采购订单这业务单据,在 MVVM 模式中实现方式的一个演示。实现方式很多,这示范也只是其中一种。这内容比较多,要分开几次讲。

说在前面,以下是用 VS 2008,.net 3.5,以及对应的 WPF Toolkit 制作。这样的话,应该绝大部分人都能应用以下例子。

MODELS

假设,系统是有供应商记录,也有物料记录,作为主数据。单据记录就是采购订单。整个业务层由这四个类组成。设计从 Model 做起,Model 来自用例,这比较自然。数据结构就这样先吧:

image

 

image

代码如下:

  1. namespace Lepton_Practical_MVVM_2.Models
  2. {
  3.     public class Supplier
  4.     {
  5.         public int Id { get; set; }
  6.         public string SupplierCode { get; set; }
  7.         public string Name { get; set; }
  8.         public string BillAddress { get; set; }
  9.         public string ShipmentAddress { get; set; }
  10.         public string ContactPerson { get; set; } // 联系人
  11.     }
  12. }

 

 

  1. namespace Lepton_Practical_MVVM_2.Models
  2. {
  3.     public class Inventory
  4.     {
  5.         public int Id { get; set; }
  6.         public string ItemCode { get; set; }
  7.         public string ItemName { get; set; }
  8.         public string Specification { get; set; }
  9.         public string Uom { get; set; } // 计量单位
  10.     }
  11. }

 

 

  1. using System;
  2.  
  3. namespace Lepton_Practical_MVVM_2.Models
  4. {
  5.     public class PurchaseOrderDetail
  6.     {
  7.         public int Id { get; set; }
  8.         public int ParentId { get; set; }
  9.         public string ItemCode { get; set; }
  10.         public decimal OrderedQty { get; set; }
  11.         public DateTime? RequestedDeliveryDate { get; set; } // 要求送货日期
  12.         public string Remark { get; set; }
  13.     }
  14. }

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4.  
  5. namespace Lepton_Practical_MVVM_2.Models
  6. {
  7.     public class PurchaseOrder
  8.     {
  9.         public int Id { get; set; }
  10.         public string DocNo { get; set; }
  11.         public DateTime DocDate { get; set; }
  12.         public string Remark { get; set; }
  13.         public string SupplierCode { get; set; }
  14.         public IList<PurchaseOrderDetail> PoDetails { get; set; } // 行明细
  15.     }
  16. }

 

采购订单有表头与明细行,两个部分组成,明细行在 PurchaseOrder 类内是 IList<T> 因为一张单可以有多行记录,一对多,我用 IList 因为准备用 NHibernate 做 ORM。你喜欢其他集合也可以。熟悉商用开发的朋友,应该对这样的结构很熟悉了。其他我不多说了。

VIEWS

 

 

 

 

 

然后看看界面,是这样样子:

image

 

呃,有点丑。咱们还是看代码吧…

 

  1. <Window x:Class="Lepton_Practical_MVVM_2.Views.Window1"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:my="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
  5.     Title="WPF MVVM 实战 - 新添加采购订单" Height="500" Width="668.772"
  6.     >
  7.     <DockPanel>
  8.         <!-- 单据表头部分 -->
  9.         <Grid DockPanel.Dock="Top" Height="200">
  10.             <ComboBox Height="23" Margin="121.465,21.435,160.048,0"
  11.                       VerticalAlignment="Top"
  12.                       ItemsSource="{Binding SupplierList}"
  13.                       DisplayMemberPath="Name"
  14.                       SelectedItem="{Binding SelectedSupplier}"
  15.                       />
  16.             <my:DatePicker Margin="121.465,58.069,160.048,0" Height="24.233"
  17.                            VerticalAlignment="Top"
  18.                            SelectedDate="{Binding DocDate}"/>
  19.             <TextBox Margin="121.465,88.598,160.048,88.598"
  20.                      Text="{Binding DocNo}"/>
  21.             <Label HorizontalAlignment="Left" Margin="6,21.435,0,0"
  22.                    Width="100.03" Height="28"
  23.                    VerticalAlignment="Top">供应商</Label>
  24.             <Label Height="28" HorizontalAlignment="Left"
  25.                    Margin="6.577,54.302,0,0" VerticalAlignment="Top"
  26.                    Width="100.03">单据日期</Label>
  27.             <Label HorizontalAlignment="Left" Margin="6.577,88.598,0,82.882"
  28.                    Width="100.03">单据号</Label>
  29.             <Label Height="28.52" HorizontalAlignment="Left"
  30.                    Margin="6.577,0,0,48.586" VerticalAlignment="Bottom"
  31.                    Width="100.03">备注</Label>
  32.             <TextBox Height="58.589" Margin="121.465,0,160.048,18.577"
  33.                      VerticalAlignment="Bottom" TextWrapping="Wrap"
  34.                      Text="{Binding DocRemark}"/>
  35.             <TextBlock Height="21" HorizontalAlignment="Right"
  36.                        Margin="0,23.435,6,0" Name="textBlock1"
  37.                        VerticalAlignment="Top" Width="146.903"
  38.                        Text="{Binding SelectedSupplier.ContactPerson}"/>
  39.         </Grid>
  40.         
  41.         <!-- 单据操作按钮部分 -->
  42.         <Grid DockPanel.Dock="Bottom" Height="50">
  43.             <Button HorizontalAlignment="Right" Margin="0,19.722,6,6"
  44.                     Width="75" Content="取消"
  45.                     Command="{Binding CloseViewCommand}"/>
  46.             <Button HorizontalAlignment="Right" Margin="0,19.722,87.169,6"
  47.                     Width="75" Content="保存"
  48.                     Command="{Binding SaveCommand}"/>
  49.         </Grid>
  50.         
  51.         <!-- 单据表体,明细行部分 -->
  52.         <DockPanel>
  53.             <!-- 添加删除行按钮 -->
  54.             <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Height="30">
  55.                 <Button Content="添加行" Margin="3,3,3,3"
  56.                         Command="{Binding AddRowCommand}"/>
  57.                 <Button Content="删除行" Margin="3,3,3,3"
  58.                         Command="{Binding DeleteRowCommand}"/>
  59.             </StackPanel>
  60.             <!-- 明细行表格 -->
  61.             <my:DataGrid CanUserAddRows="False"
  62.                           AutoGenerateColumns="False"
  63.                           ItemsSource="{Binding purchaseOrder.PoDetails}"
  64.                           SelectedItem="{Binding CurrentRow}" >
  65.                 <my:DataGrid.Resources>
  66.                     <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
  67.                                      Color="LightBlue"/>
  68.                 </my:DataGrid.Resources>
  69.                 <my:DataGrid.Columns>
  70.                     
  71.                     <my:DataGridTemplateColumn Header="物料号">
  72.                         <my:DataGridTemplateColumn.CellEditingTemplate>
  73.                             <DataTemplate>
  74.                                 <StackPanel Orientation="Horizontal">
  75.                                     <TextBlock MinWidth="100" Text="{Binding ItemCode}"/>
  76.                                     <Button Content="..."
  77.                                             Command="{Binding ItemCodeSelectionCommand}"/>
  78.                                 </StackPanel>
  79.                             </DataTemplate>
  80.                         </my:DataGridTemplateColumn.CellEditingTemplate>
  81.                         <my:DataGridTemplateColumn.CellTemplate>
  82.                             <DataTemplate>
  83.                                 <TextBlock MinWidth="100" Text="{Binding ItemCode}"/>
  84.                             </DataTemplate>
  85.                         </my:DataGridTemplateColumn.CellTemplate>
  86.                     </my:DataGridTemplateColumn>
  87.                     
  88.                     <my:DataGridTextColumn Header="数量" Binding="{Binding OrderedQty}"/>
  89.                     
  90.                     <my:DataGridTemplateColumn Header="要求交货日期">
  91.                         <my:DataGridTemplateColumn.CellEditingTemplate>
  92.                             <DataTemplate>
  93.                                 <my:DatePicker SelectedDate="{Binding RequestedDeliveryDate}"/>
  94.                             </DataTemplate>
  95.                         </my:DataGridTemplateColumn.CellEditingTemplate>
  96.                         <my:DataGridTemplateColumn.CellTemplate>
  97.                             <DataTemplate>
  98.                                 <TextBlock Text="{Binding RequestedDeliveryDate, StringFormat=dd/MM/yyyy}"/>
  99.                             </DataTemplate>
  100.                         </my:DataGridTemplateColumn.CellTemplate>
  101.                     </my:DataGridTemplateColumn>
  102.                     
  103.                     <my:DataGridTextColumn Header="备注" Width="200"
  104.                                            Binding="{Binding Remark}"/>
  105.                     
  106.                 </my:DataGrid.Columns>
  107.             </my:DataGrid>
  108.         </DockPanel>
  109.     </DockPanel>
  110. </Window>

整个布局,用 DockPanel,分上中下三个部分,分别用来放置表头,明细行,和操作按钮。明细行区域又用了 DockPanel 再分开了添加行、删除行按钮区域,和明细行的 GridView。整个 XAML 我唯一调过样式的,是 GridView 的当前行高亮底色,原来的蓝色实在太刺眼了。

全部绑定都是写 Path,因为整个 Window 的 DataContext 就是 ViewModel,它提供一切数据(或者负责指向实际业务类的实例)。我假设大家会用 Template,会一般的绑定,不解释了。

VIEWMODELS

然后是 ViewModel,我从表头开始讲。

里面比较有趣的,是一个 Combo Box,它应该出现的选项,是 Supplier 业务类的集合。再看看 PurchaseOrder 这个类的结构,用户选了 Supplier 之后,放进去 PurchaseOrder 不是 Supplier 实例,而是 SupplierCode 。

image

除此之外,看看 XAML ,我还搞了一个 TextBlock 在 Combo Box 旁边,用来显示一些关于这 Supplier 供应商的额外信息,比如我显示了联系人。

image

image

要做到这两点需求,不能单靠 Path 绑来绑去就能解决,我需要一个已选择了的供应商对象,存放在 ViewModel,然后在 TextBlock 绑过去,用 Path 指定要显示信息的路径。在我这例子,这已选定的供应商属性,变量名是 SelectedSupplier,我要显示联系人,所以整个 Binding 的路径就是 SelectedSupplier.ContactPerson,见 XAML 第 38 行。

我认为这做法的好处是,如果有哪天你需要更多关于该选定供应商的信息,显示在界面,你 ViewModel 啥都不用改,只在 View 的 XAML 加个控件设一下路径即可。

然后,选定的供应商,是这样传到 SelectedSupplier 属性的:

image

 

 

那么,供应商编号,又是如何传进去 Model (PurchaseOrder)的 SupplierCode 呢?就在 ViewModel 的 SelectedSupplier 中 Setter 代码,这里:

image

每一次选定的供应商变化,由 Combo Box 绑定至 SelectedSupplier,而它除了更新属性值以外,还同时更新到业务对象 PurchaseOrder 的实例属性 SupplierCode 内。

整个 Combo Box 和它的“额外信息”,就是这样处理了。

看看到目前为止的 ViewModel 代码,数据层代码我不贴出来了,我只是做了些假数据让数据层提供而已。我代码内的 FillSupplierList 方法,也应该开线程来读,各位自己注意一下自己改吧。其他部分下次继续…

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Collections.ObjectModel;
  6. using System.Windows.Input; // ICommand
  7. using IPE.Framework.UI.ViewModels; // ViewModelBase
  8. using IPE.Framework.UI.Commands; // RelayCommand
  9.  
  10. namespace Lepton_Practical_MVVM_2.ViewModels
  11. {
  12.     public class MainWindowViewModel : ViewModelBase
  13.     {
  14.         public MainWindowViewModel()
  15.         {
  16.             Initialize();
  17.         }
  18.  
  19.         private void Initialize()
  20.         {
  21.             purchaseOrder = new Models.PurchaseOrder();
  22.             purchaseOrder.PoDetails = new ObservableCollection<Models.PurchaseOrderDetail>();
  23.             SupplierList = new ObservableCollection<Models.Supplier>();
  24.  
  25.             FillSupplierList();
  26.         }
  27.  
  28.         private void FillSupplierList()
  29.         {
  30.             List<Models.Supplier> customerlist = DataAccess.DataProvider.GetAllCustomers();
  31.             foreach (Models.Supplier customer in customerlist)
  32.             {
  33.                 this.SupplierList.Add(customer);
  34.             }
  35.             customerlist = null;
  36.         }
  37.  
  38.         #region Acutal Model Object reference
  39.         public Models.PurchaseOrder purchaseOrder { get; set; }
  40.         #endregion
  41.  
  42.         #region Supplier Selection Combo Box
  43.  
  44.         private Models.Supplier selectedSupplier;
  45.         public Models.Supplier SelectedSupplier
  46.         {
  47.             get { return selectedSupplier; }
  48.             set
  49.             {
  50.                 if (selectedSupplier != value)
  51.                 {
  52.                     selectedSupplier = value;
  53.                     purchaseOrder.SupplierCode = value.SupplierCode;
  54.                     OnPropertyChanged("SelectedSupplier");
  55.                 }
  56.             }
  57.         }
  58.  
  59.         public ObservableCollection<Models.Supplier> SupplierList { get; set; }
  60.  
  61.         #endregion
  62.  
  63.         // 待续 ...
  64.     }
  65. }

 

效果图:

image

image

No comments: