一、场景
有过WPF项目经验的朋友可能都知道,如果一个DataGrid要绑定静态的数据是非常的简单的(所谓静态是指绑定的数据源的类型是静态的),如下图所示,想要显示产品数据,只需绑定到一个产品列表即可,这个大家都清楚,所以这个要讲的肯定不是这个。
但是现在有一个新的需求,根据所选择产品的不同,要动态生成第二个表格中的不同数据,以便进行编辑,如下图1、2所示,当选择的产品不同时,第二个表格显示的内容是完全不一样的。
这样就会产生一个问题,无法直接对第二个表格进行绑定,因为它的数据源类型都是不一样的,无法按照传统方法进行绑定。如何解决,先自己思考一下,也许会有更好的解决方案。
二、思路
1、定义Domain
既然无法知道要绑定的数据类型是什么,因为它是动态的,无法事先预知的,根据所选择的产品(因为产品数据都存储于DB中,所以将类型定义于Domain项目中)不同而变化的。那么可以在 Product 中定义 SKUFields 属性用于代表产品可显示的字段。产品定义如下所示。
/// <summary> /// 产品 /// </summary> [Table("Product")] public class Product { public int ProductId { get; set; } public string ProductDesc { get; set; } public bool IsRFID { get; set; } public bool IsTID { get; set; } public string CustomerItemCode { get; set; } public string ProductCode { get; set; } public string SuggestedPrinter { get; set; } /// <summary> /// 产品对应的文件名称 /// </summary> public string ProfileName { get; set; } /// <summary> /// 产品对应的SKU字段列表 /// </summary> public ICollection<SKUField> SKUFields { get; set; } }
/// <summary> /// SKU 字段 /// </summary> public class SKUField { /// <summary> /// 产品ID /// </summary> public int ProductId { get; set; } /// <summary> /// 字段ID /// </summary> public int SKUFieldId { get; set; } /// <summary> /// 字段名称 /// </summary> public string FieldName { get; set; } /// <summary> /// 字段标题,用于显示 /// </summary> public string FieldTitle { get; set; } /// <summary> /// 字段是否可编辑 /// </summary> public bool IsEditable { get; set; } /// <summary> /// 字段的值 /// </summary> public object DefaultValue { get; set; } /// <summary> /// 字段值是否可选 /// </summary> public bool IsValueSelectable { get; set; } /// <summary> /// 字段的类型,如int、string或其他 /// </summary> public Type FieldType { get; set; } /// <summary> /// 如果IsValueSelectable = True,那么SKUFieldValues代表可选择的值列表 /// </summary> public ICollection<SKUFieldValue> SKUFieldValues { get; set; } }
/// <summary> /// 用于定义SKU字段的取值 /// </summary> public class SKUFieldValue { public int SKUFieldId { get; set; } public int SKUFieldValueId { get; set; } public string FieldValue { get; set; } }
然后在项目中定义 IProductRepository 接口,用于定义获取产品的方法。
public interface IProductRepository { /// <summary> /// 获取默认的产品列表 /// </summary> /// <returns></returns> IEnumerable<Product> GetDefaultProducts(); /// <summary> /// 获取特定客户账号的产品列表 /// </summary> /// <param name="account">客户账号如YTST02DY、G99999CG</param> /// <returns></returns> IEnumerable<Product> GetProductsByAccount(string account); }
Domain项目结构所图所示。
2、定义服务,用于获取所有产品信息
第二部建立 Application 项目,用于提供 WPFUI 项目所需要的服务,这里我不打算使用真实的数据源来提供数据,所以只是使用 Mock 来模拟服务。
/// <summary> /// 产品接口,用于提供产品数据 /// </summary> public interface IProductService { IEnumerable<Product> GetAllProducts(); }
/// <summary>
/// 真实的服务,使用EF框架来获取数据,但是这里不合适这个服务,为了方便演示
/// </summary>
public class ProductService : IProductService { public IEnumerable<Product> GetAllProducts() { using (var context = new SQLiteDataContext()) { return context.Products.ToList(); } } }
/// <summary>
/// 模拟的服务,为了方便演示
/// </summary>
public class MockProductService : IProductService { public IEnumerable<Product> GetAllProducts() { var product2 = GetProduct2(); var product3 = GetProduct3(); var product4 = GetProduct4(); return new List<Product>() { product2, product3, product4 }; } public Profile GetProfile(Product product) { string filePath = Path.Combine(string.Format(@"Resources\Products\{0}", product.ProductCode), product.ProfileName); Profile profile = new Profile(filePath, TempFolder.CreateTempFolder()); return profile; } private Product GetProduct2() { var product = new Product() { ProductId = 2, IsRFID = true, IsTID = true, CustomerItemCode = "FP-PT#003", ProductDesc = "Coated Stock", ProductCode = "88CEMPH006", ProfileName = "88CEMPH006.spkg", SuggestedPrinter = "SMLPrinter" }; product.SKUFields = new List<SKUField>(); SKUField skuField1 = new SKUField(); skuField1.FieldName = "Qty"; skuField1.FieldTitle = "Order Qty"; skuField1.FieldType = typeof(System.Int32); skuField1.IsEditable = true; product.SKUFields.Add(skuField1); SKUField skuField2 = new SKUField(); skuField2.FieldName = "Size"; skuField2.FieldTitle = "Size"; skuField2.FieldType = typeof(System.String); skuField2.IsEditable = true; skuField2.IsValueSelectable = true; skuField2.SKUFieldValues = new List<SKUFieldValue>() { new SKUFieldValue() { FieldValue = "Large" }, new SKUFieldValue() { FieldValue = "Middle" }, new SKUFieldValue() { FieldValue = "Small" }, }; product.SKUFields.Add(skuField2); SKUField skuField3 = new SKUField(); skuField3.FieldName = "Retail"; skuField3.FieldTitle = "Retail"; skuField3.FieldType = typeof(System.String); skuField3.IsEditable = true; product.SKUFields.Add(skuField3); return product; } private Product GetProduct3() { var product = new Product() { ProductId = 3, IsRFID = false, IsTID = false, CustomerItemCode = "FP-PT#004", ProductDesc = "Coated Stock", ProductCode = "88CEMNH006", ProfileName = "88CEMNH006.spkg", SuggestedPrinter = "SML FP300R (Copy 1)", }; product.SKUFields = new List<SKUField>(); SKUField skuField1 = new SKUField(); skuField1.FieldName = "Qty"; skuField1.FieldTitle = "Order Qty"; skuField1.FieldType = typeof(System.Int32); skuField1.IsEditable = true; product.SKUFields.Add(skuField1); SKUField skuField2 = new SKUField(); skuField2.FieldName = "Size"; skuField2.FieldTitle = "Size"; skuField2.FieldType = typeof(System.String); skuField2.IsEditable = true; skuField2.IsValueSelectable = true; skuField2.SKUFieldValues = new List<SKUFieldValue>() { new SKUFieldValue() { FieldValue = "Large" }, new SKUFieldValue() { FieldValue = "Middle" }, new SKUFieldValue() { FieldValue = "Small" }, }; product.SKUFields.Add(skuField2); SKUField skuField3 = new SKUField(); skuField3.FieldName = "Style"; skuField3.FieldTitle = "Style"; skuField3.FieldType = typeof(System.String); skuField3.IsEditable = true; skuField3.IsValueSelectable = true; skuField3.SKUFieldValues = new List<SKUFieldValue>() { new SKUFieldValue() { FieldValue = "001" }, new SKUFieldValue() { FieldValue = "002" }, new SKUFieldValue() { FieldValue = "003" }, }; product.SKUFields.Add(skuField3); SKUField skuField4 = new SKUField(); skuField4.FieldName = "CollectionName"; skuField4.FieldTitle = "Collection Name"; skuField4.FieldType = typeof(System.String); skuField4.IsEditable = false; skuField4.DefaultValue = "100% COTTON"; product.SKUFields.Add(skuField4); return product; } private Product GetProduct4() { var product = new Product() { ProductId = 4, IsRFID = false, IsTID = false, CustomerItemCode = "FP-PT#004", ProductDesc = "Coated Stock", ProductCode = "88CEMNH004", ProfileName = "88CEMNH004.spkg", SuggestedPrinter="Fax", }; product.SKUFields = new List<SKUField>(); SKUField skuField1 = new SKUField(); skuField1.FieldName = "Qty"; skuField1.FieldTitle = "Order Qty"; skuField1.FieldType = typeof(System.Int32); skuField1.IsEditable = true; product.SKUFields.Add(skuField1); SKUField skuField2 = new SKUField(); skuField2.FieldName = "Size"; skuField2.FieldTitle = "Size"; skuField2.FieldType = typeof(System.String); skuField2.IsEditable = true; skuField2.IsValueSelectable = true; skuField2.SKUFieldValues = new List<SKUFieldValue>() { new SKUFieldValue() { FieldValue = "Large" }, new SKUFieldValue() { FieldValue = "Middle" }, new SKUFieldValue() { FieldValue = "Small" }, }; product.SKUFields.Add(skuField2); SKUField skuField3 = new SKUField(); skuField3.FieldName = "Style"; skuField3.FieldTitle = "Style"; skuField3.FieldType = typeof(System.String); skuField3.IsEditable = true; skuField3.IsValueSelectable = true; skuField3.SKUFieldValues = new List<SKUFieldValue>() { new SKUFieldValue() { FieldValue = "001" }, new SKUFieldValue() { FieldValue = "002" }, new SKUFieldValue() { FieldValue = "003" }, }; product.SKUFields.Add(skuField3); SKUField skuField4 = new SKUField(); skuField4.FieldName = "CollectionName"; skuField4.FieldTitle = "Collection Name"; skuField4.FieldType = typeof(System.String); skuField4.IsEditable = false; skuField4.DefaultValue = "100% COTTON"; product.SKUFields.Add(skuField4); return product; } }
项目结构如图所示
3、定义WPF项目,用于显示UI
Note: 项目有使用CM、Ninject框架
项目结构如下
(1) 定义Model类
UISKURecord 用于定义第二个表格的数据源中的一行数据,包含 UISKUField 列表,用于代表不确定的列。
public class UISKURecord { private readonly ObservableCollection<UISKUField> _uiSKUFields = new ObservableCollection<UISKUField>(); public ObservableCollection<UISKUField> UISKUFields { get { return _uiSKUFields; } } public void AddSKUField(UISKUField uiSKUField) { _uiSKUFields.Add(uiSKUField); } }
UISKUField 用于定义要显示的属性,继承自SKUField中,添加了两个用于绑定UI的属性。
public class UISKUField : SKUField, INotifyPropertyChanged { /// <summary> /// 两种情况下UI文本框绑定此属性 /// 1. IsEditable = False /// 2. IsEditable = True But IsValueSelectable = False /// </summary> public object Value { get; set; } /// <summary> /// 当IsValueSelectable = True时,UI显示下拉列表供用户选择值 /// 此时下拉列表SelectedItem绑定此属性 /// </summary> private SKUFieldValue _selectedUISKUFieldValue; public SKUFieldValue SelectedUISKUFieldValue { get { return _selectedUISKUFieldValue; } set { _selectedUISKUFieldValue = value; OnPropertyChanged(); } } #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } #endregion }
SKUFieldExtensionMethods用于定义将服务提供的数据类型转化成UI绑定所使用的数据类型。
public static class SKUFieldExtensionMethods { public static UISKUField ToUISKUField(this SKUField skuField) { UISKUField uiSKUField = new UISKUField(); uiSKUField.Value = skuField.DefaultValue; uiSKUField.FieldName = skuField.FieldName; uiSKUField.FieldTitle = skuField.FieldTitle; uiSKUField.IsEditable = skuField.IsEditable; uiSKUField.IsValueSelectable = skuField.IsValueSelectable; if (uiSKUField.IsValueSelectable) { uiSKUField.SKUFieldValues = skuField.SKUFieldValues; uiSKUField.SelectedUISKUFieldValue = uiSKUField.SKUFieldValues.FirstOrDefault(); } return uiSKUField; } }
(2) 定义 ViewModel 类
public class MainViewModel : PropertyChangedBase { #region Field private IProductService _productService = null; #endregion #region Ctor public MainViewModel(IProductService productService) { _productService = productService; SKURecords = new ObservableCollection<UISKURecord>(); Products = new ObservableCollection<Product>(_productService.GetAllProducts()); } #endregion #region Prop private ObservableCollection<Product> _products; /// <summary> /// 所有产品 /// </summary> public ObservableCollection<Product> Products { get { return _products; } set { _products = value; SelectedProduct = value.FirstOrDefault(); NotifyOfPropertyChange(() => Products); } } private Product _selectedProduct; /// <summary> /// 所选产品 /// </summary> public Product SelectedProduct { get { return _selectedProduct; } set { _selectedProduct = value; NotifyOfPropertyChange(() => SelectedProduct); //切换产品时,先清空SKU表格中的数据,再添加一行 SKURecords.Clear(); AddSKU(); } } private ObservableCollection<UISKURecord> _skuRecords; public ObservableCollection<UISKURecord> SKURecords { get { return _skuRecords; } set { _skuRecords = value; NotifyOfPropertyChange(() => SKURecords); } } private UISKURecord _selectedSKURecord; public UISKURecord SelectedSKURecord { get { return _selectedSKURecord; } set { _selectedSKURecord = value; NotifyOfPropertyChange(() => SelectedSKURecord); } } #endregion #region ICommand Method public void AddSKU() { UISKURecord skuRecord = new UISKURecord(); foreach (var skuField in _selectedProduct.SKUFields) { skuRecord.AddSKUField(skuField.ToUISKUField()); } SKURecords.Add(skuRecord); } #endregion }
(3) 定义View
<Window x:Class="WpfApplication3.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApplication3" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <DataTemplate x:Key="customerDataGridTextBlockColumnDataTemplate"> <TextBlock Text="{Binding Path = Value}"></TextBlock> </DataTemplate> <DataTemplate x:Key="customerDataGridTextColumnDataTemplate"> <TextBox Text="{Binding Path = Value}"></TextBox> </DataTemplate> <DataTemplate x:Key="customerDataGridComboboxColumnDataTemplate"> <ComboBox ItemsSource="{Binding Path = SKUFieldValues}" DisplayMemberPath="FieldValue" SelectedItem="{Binding SelectedUISKUFieldValue}"> </ComboBox> </DataTemplate> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> <RowDefinition Height="30"/> </Grid.RowDefinitions> <DataGrid x:Name="ProductsDataGrid" AutoGenerateColumns="False" ItemsSource="{Binding Products}" SelectionChanged="ProductsDataGrid_SelectionChanged" SelectedItem="{Binding SelectedProduct}" Margin="10"> <DataGrid.Columns> <DataGridTextColumn Header="Product Code" Width="150" Binding="{Binding Path=ProductCode}"/> <DataGridTextColumn Header="Customer Item Code" Width="150" Binding="{Binding Path=CustomerItemCode}"/> <DataGridTextColumn Header="Product Desc" Width="150" Binding="{Binding Path=ProductDesc}"/> </DataGrid.Columns> </DataGrid> <DataGrid x:Name="SKUsDataGrid" Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding SKURecords}" SelectedItem="{Binding SelectedSKURecord}" Margin="10" CanUserAddRows="False" CanUserDeleteRows="False"> </DataGrid> <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Left" Margin="10 0 0 0"> <Button x:Name="AddSKU" Content="Add" Width="75" Height="25" Margin="0,0,0,2" VerticalAlignment="Bottom"/> <Button x:Name="RemoveSKU" Content="Remove" Width="75" Height="25" Margin="5 0 0 0"/> </StackPanel> </Grid> </Window>
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { MainViewModel _viewModel; public MainWindow() { InitializeComponent(); this.DataContext = _viewModel = new MainViewModel(); } private void ProductsDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { SKUsDataGrid.Columns.Clear(); var viewModel = (MainViewModel)DataContext; var fields = _viewModel.SKURecords.First().SKUProperties.ToList(); for (int i = 0; i < fields.Count; i++) { var field = fields[i]; //列不可编辑 if (!field.IsEditable) { var column = new CustomBoundColumn(); column.IsReadOnly = true; column.Header = field.FieldTitle; column.Binding = new Binding(string.Format("SKUProperties[{0}]", i)); column.TemplateName = "customerDataGridTextBlockColumnDataTemplate"; column.Width = 100; SKUsDataGrid.Columns.Add(column); } else { if (!field.IsValueSelectable) { var column = new CustomBoundColumn(); column.IsReadOnly = false; column.Header = field.FieldTitle; column.Binding = new Binding(string.Format("SKUProperties[{0}]", i)); column.TemplateName = "customerDataGridTextColumnDataTemplate"; column.Width = 100; SKUsDataGrid.Columns.Add(column); } else { var column = new CustomBoundColumn(); column.IsReadOnly = false; column.Header = field.FieldTitle; column.Binding = new Binding(string.Format("SKUProperties[{0}]", i)); column.TemplateName = "customerDataGridComboboxColumnDataTemplate"; column.Width = 100; SKUsDataGrid.Columns.Add(column); } } } } }
public class CustomBoundColumn : DataGridBoundColumn { public string TemplateName { get; set; } protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) { var binding = new Binding(((Binding)Binding).Path.Path); binding.Source = dataItem; var content = new ContentControl(); content.ContentTemplate = (DataTemplate)cell.FindResource(TemplateName); content.SetBinding(ContentControl.ContentProperty, binding); return content; } protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) { return GenerateElement(cell, dataItem); } }