Saturday, July 21, 2012

C# WPF MVVM 实战 – 2.4 单元测试

原文发布在博客园:http://www.cnblogs.com/leptonation/archive/2012/06/28/2567387.html

image

接上篇,我留到这里才介绍怎样测试,因为不会做的话也不会测做得对不对。说是单元测试的话,其实应该设计好 Model 后,定好大概 VM 内要干什么之后,马上可以动手写测试代码。

很多公司没有规定如何测试,更加没有单元测试,也没考虑 TDD,或许是有他们的原因的,不一定是因为水平问题的,也不一定是同事能力所限。还真的有很多人,很多老开发,不愿意写测试。这测试只是一种开发方法,是其中一个而已。没有所谓绝对的最佳办法。

我这里说的,是一些情况,你必须要这么测,才有比较好的效率,才能确保代码正确无误。

想一想,如果你负责写模块,同时,另一位负责写 Shell,(比如用 PRISM 的 Modularity),你在没有 Shell 的情况下,点击 Debug Run 帮不了你。你的 View 全是 UserControl,你的入口,是个 IModule 的东西而且它只负责注册视图。你真要运行来看效果来「人肉」测试的话,你只能自己写一个 Shell 出来(?!),在 IModule 注册菜单后,再由 Shell 打开视图。

一个只为测试而做的 Shell ,可能很快能写出来。同样地,一个测试项目也一样很快能写完。它们之间的分别,是测试项目能自动重复运行。

如果 TDD 就更加不用说了。

废话讲完。入正题。

测试对象:ICommand 的 CanExecute

假设,用例之一,是「在没有选择明细行前,删除行按钮禁用」。在上几篇实现了的一个功能。

以下省略了测试描述,只含代码:

image

有没有选中明细行,这是视图中 DataGrid 对它 SelectedItem 属性(绑定 VM 的 CurrentRow)赋值的一个操作,可以像上面代码一样模拟它。注意,如果你 VM 构造函数,有检查参数是否为 null 然后是 null 就抛异常的话,上面代码就当然不行,要写个模拟,如下部分的做法。

上面代码,除了 new 什么出来测以外,真正的代码只有两行,一个是为 CurrentRow 赋值,另一个是运行 CanExecute 委托让它返回 true/false。额,当然还有 assertion 啦。

这测试,其实只是检查 { return CurrentRow != null; } 这一句委托而已,作用不大,但想象一下,如果这是 VM 包含了数据验证用了 IDataErrorInfo,测试目标是保存按钮能不能点击,那委托,就不是那么简单了。

测试对象:ICommand 的 Execute

假设,用例中,要求明细行内,单行中的物料栏,要能打开另一个选择窗口来选择物料

image

这里做了几件事,这几件事是从用例而来,是操作的顺序。target.ItemCodeSelectionCommand 没有必要 Execute,运行了也只是有个空白窗口,标题是 Mock Empty Window 的而已,写这样纯粹为示范模拟类用到依赖注入上。

这测试能通过,但或许会有错误信息,不过你看看就知道是 BackgroundWorker 的事,我不作解释了,它跟这里无关。

其他

额,其他我不说了。测属性没什么好说,特别是在这里没数据验证的情况下。

总结

我觉得测试中,MVVM 跟其他的,不一样的地方就是这个 ICommand。上面写了它两方面的测试运作。我就单单写这部分算了。

测试中,没有调用采购订单的视图,连弹出窗口也是假的,这测试,要测的是 ViewModel 本身。MVVM 在单独测试有优势,就是 ViewModel 它自己能做到完全与 View 隔开,单独运行。好处是:

  1. 外壳没写就已经开始写和测试 ViewModel
  2. 弹出窗口没写也可以写和测试 ViewModel
  3. 减少了依赖,发生错误时,查找的范围只限于 ViewModel 本身
  4. 每次改动后,重复测试,能确保 ViewModel 按照原定的行为跑

至于 Model 呢,Model 本身只是个放值的容器,除 get /set 外没有任何东西,如果 EF 等等的 ORM 它更是自动生成,我认为没必要分离出来,它错的机会低,而且分出来也有相当大的工作量。

再说一篇这只是其中一个方法,再说这做法与外面、书本上说的不一样,有点歪曲了的做法。对我管用。实际用不用,请自行考虑。

采购订单示例,到此结束。往后的是,MVVM 应用时的各种情况,下一篇将会是树结构在 MVVM 的绑定。

树结构很有趣,很多算法都跟它有关,它也随处可见,比如菜单,比如产品结构 BOM 、组织架构等等。虽然有趣,但用起来痛苦,过往你试过写代码历遍 node 找一个元素,然后一堆 boxing / unboxing,你懂的。用 MVVM 会是一个解脱,一部分解脱。下回继续。

C# WPF MVVM 实战 – 2.3

原文发布在博客园:http://www.cnblogs.com/leptonation/archive/2012/06/20/2557108.html

上一篇介绍了增加删除行可以怎样做,现在说填写时候,在某一栏让用户选择,选项的集合是每行数据共用。这次我尽量说得不那么 Hello World。想说说一个真的系统内,你或许要的一些设计、一些你需要做的决定。

技术上,这次有 :

  1. BackgroundWorker 加载列表
  2. 消除采购订单 ViewModel 对另一个它自己要打开的 View 的依赖
  3. 要 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,用它比较省事。

image

image

image

读数据跟 MVVM 关系不大。留意一下 InventoryListWorker.RunWorkerCompleted,我把结果放进去两个 List<T>,子窗体过滤时候用,一个是全的、代码不动它的,一个是用户过滤后的。这样比较省事。

采购订单 ViewModel 加入打开子窗体的命令

代码逻辑很简单,Modal Window 用 ShowDialog() 打开,连查看 ShowDialog() 回传的 true/false 都省掉了,因为是同一个 VM,子窗体直接操作采购订单 VM 内的明细行,下部分讲。

 

 

image

或许看到这里,new 关键字,子窗体类(View)出现在 VM 代码,马上眉头皱。这不是 MVVM 哦。这里 VM 对 View 依赖。

一下重构就把它灭了。要把子窗体分离出来,想不 new 它,你可以写视图服务让它提供,工厂之类,有 IoC 容器或许更方便。要做不做,你在不在乎这依赖,请自行判断。

image

image

然后,像这示例没 IoC 容器的,只能在外面 new 咯。

image

这样做,你不需要任何 View,就可以测试这 MainWindowViewModel 类。测试下一篇讲。

 

 

子窗体的属性与命令

首先看看这子窗体的外貌:

image

两个输入框用来拿过滤用(绑定两个 string 属性),两个按钮(绑定两个 ICommand),一个 DataGrid (绑定过滤后的集合)。还有一个,DataGrid 内 Double Click 命令绑定,让用户双击选物料。

对 MVVM 开始理解了的话,下面这些就很简单。我过滤没有什么算法,直接 LINQ,然后替换集合而已。唯一麻烦的是,Double Click 的绑定。

image

image

image

image

子窗体的 DataGrid 双击绑定

关于窗体,我唯一想说的,是双击。

首先,微软有病。控件的命令,为何有时候是指定一定要 RoutedCommand,有时候又可以 ICommand。我不知道,问他们不要问我。

要解决,闲着没事自己写 Attached Property + 转换,或者,下个源码,测试过你喜欢的,或许改一下,收录进去代码库。你会经常用到的。

我拿个 BehaviorBinding 的代码来改。原版它本身有个问题,就是你绑定没写或者名字错,绑不到,它会抛异常。这与其他属性绑定的行为不一样。你觉得没什么大不了的话,直接用。让它吃掉异常这很好改,我这部分不公开了。原版的源码好像是这里的,我不太记得,打不开请翻墙,或者自行 Google。

image

 

我做法,两步,第一是 Preview 让它执行 ICommand,第二是这个 View 的 DataGrid_MouseDoubleClick。第二步是干嘛?没干嘛:

image

重要部分的源码

讲代码部分,终于结束。其实代码挺简单的,几百行其实大部分都是属性的 get/set,说那么多其实是想让大家在决定是否用 MVVM 时候心里有个底,也希望大家有个概念怎样搞。下篇才讲测试。低耦合人人爱之余,我认为测试也是 MVVM 的重点所在。

app.xaml.cs

 

namespace Lepton_Practical_MVVM_2 {
    /// <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

 

<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}" 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

 

namespace Lepton_Practical_MVVM_2.ViewModels {
    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

namespace Lepton_Practical_MVVM_2.Views {
    public interface IViewProvider {
        System.Windows.Window GetItemCodeSelectionWindow();
    }
}

 

ViewProvider.cs

 

namespace Lepton_Practical_MVVM_2.Views {
    public class ViewProvider :IViewProvider{

        #region IViewProvider Members

        public System.Windows.Window GetItemCodeSelectionWindow() {
            return new ItemCodeSelectionWindow();
        }

        #endregion
    }
}

 

 

ItemCodeSelectionWindow.xaml

 

<Window x:Class="Lepton_Practical_MVVM_2.Views.ItemCodeSelectionWindow"
    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>