• WPF 动态生成DataGrid及动态绑定解决方案


    一、场景

    有过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(@"ResourcesProducts{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);
        }
    }
  • 相关阅读:
    Yarn安装与配置
    如何找到所有 HTML select 标签的选中项
    Jquery 获取某个样式除第一个以外的元素
    常用的字符串截取方法
    App的埋点测试
    手写 promise
    关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑
    计算一个数组的深度
    数组的 交集 差集 补集 并集
    webpack -- element-ui 的按需引入
  • 原文地址:https://www.cnblogs.com/JustYong/p/6558511.html
Copyright © 2020-2023  润新知