C# WPF MVVM 实战 – 2.3
上一篇介绍了增加删除行可以怎样做,现在说填写时候,在某一栏让用户选择,选项的集合是每行数据共用。想说说一个真的系统内,你或许要的一些设计、一些你需要做的决定。
技术上,这次有 :
- 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 的代码来改。原版它本身有个问题,就是你绑定没写或者名字错,绑不到,它会抛异常。这与其他属性绑定的行为不一样。你觉得没什么大不了的话,直接用。让它吃掉异常这很好改,我这部分不公开了。原版的源码好像是这里的,我不太记得,打不开请FQ,或者自行 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>