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

泪湿孤枕 提交于 2020-03-04 21:51:38

一、场景

有过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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!