简体   繁体   English

DataGridView 数据绑定到列表<list<t> &gt; </list<t>

[英]DataGridView Databinding to List<List<T>>

Given the code给定代码

class Foo {
    public string Value {get; set;}
    public int Id {get; set;}
}
List<List<Foo>> fooList = new List<List<Foo>>();

Is there a way to bind a Multidim ICollection to a DataGridView on the property Value, where when you change a cell, the Value property of the object updates?有没有办法将 Multidim ICollection 绑定到属性 Value 上的 DataGridView,当您更改单元格时,object 的 Value 属性会更新?

In this case, each instance of Foo in the list will represent one cell in the DataGridView and the rows/ columns are being preserved as they would be in the multidim ICollection在这种情况下,列表中的每个 Foo 实例将代表 DataGridView 中的一个单元格,并且行/列将被保留,就像它们在 multidim ICollection 中一样

By Multidim I mean something to the affect of:通过 Multidim,我的意思是影响:

List<List<Foo>] => [
    List<Foo> => [0,1,2,3,4,5]
    List<Foo> => [0,1,2,3,4,5]
    List<Foo> => [0,1,2,3,4,5]
    List<Foo> => [0,1,2,3,4,5]
]

Where each element in the nested list is actually and instance of Foo.嵌套列表中的每个元素实际上都是 Foo 的实例。

The problem you describe can be solved a couple of different ways.您描述的问题可以通过几种不同的方式解决。 One is to “flatten” each List<Foo>.一种是“扁平化”每个List<Foo>. Basically this will flatten ALL the Foo items in a list into a “single” string.基本上,这会将列表中的所有Foo项目展平为“单个” string. With this approach as I commented, you would end up with one column and each row would be a “flattened” List<Foo>.正如我所评论的那样,使用这种方法,你最终会得到一列,每一行都是一个“扁平化”的List<Foo>. Each cell may have a different number of Foo items in the string.每个单元格在字符串中可能有不同数量的Foo项。

In this case and as others, this may not be the desired result.在这种情况下和其他情况下,这可能不是预期的结果。 Since you have a List of Lists , then a “Master-Detail" approach using two (2) grids may make things easier. In this approach, the first grid (master) would have one (1) column and each row would be a List<Foo>. Since we already know the grid will not display this LIST into a single cell AND we don't want to “flatten” the list, then this is where the second (detail) grid comes into play. The first grid displays all the lists of Foo , and whichever “row” is selected, the second grid (detail) will display all the List<Foo> items.由于您有一个List of Lists ,因此使用两 (2) 个网格的“主从”方法可能会使事情变得更容易。在这种方法中,第一个网格(主)将有一个 (1) 列,每行将是一个List<Foo>.由于我们已经知道网格不会将此 LIST 显示为单个单元格,并且我们不想“扁平化”列表,因此这是第二个(详细)网格发挥作用的地方。第一个网格显示Foo的所有列表,无论选择哪个“行”,第二个网格(详细信息)将显示所有List<Foo>项目。

An example may work best to show what I mean.一个例子可能最能说明我的意思。 First, we need to make an additional class.首先,我们需要制作一个额外的 class。 Reason being that is if we use a List<List<Foo>> as a DataSource to the master grid, it will show something like…原因是如果我们使用List<List<Foo>>作为主网格的DataSource ,它将显示类似...

在此处输入图像描述

As shown the two columns are going to be the List “Capacity” and “Count.”如图所示,这两列将是List “容量”和“计数”。 This may work;这可能有效; however, it may be confusing to the user.但是,它可能会使用户感到困惑。 That is why we want this other class.这就是为什么我们想要另一个 class。 It is a simple “wrapper” around the List<Foo> and to display this we will add a “Name” property to this class.它是List<Foo>周围的简单“包装器”,为了显示它,我们将向此 class 添加一个“Name”属性。 This will be displayed in the master grid.这将显示在主网格中。

Given the current modified Foo class…鉴于当前修改后的Foo类...

public class Foo {
  public string Value { get; set; }
  public int Id { get; set; }
}

This FooList class may look something like…这个FooList class 可能看起来像……

public class FooList {
  public string ListName { get; set; }
  public List<Foo> TheFooList { get; set; }
}

A List<FooList> would display something like… List<FooList>会显示类似...

在此处输入图像描述

Now, when the user “selects” a row in the first “Master” grid, the second “Detail" grid will display all the Foo items in that list. A full example is below. Drop two grids onto a form and copy the code below to follow.现在,当用户在第一个“主”网格中“选择”一行时,第二个“细节”网格将显示该列表中的所有Foo项目。下面是一个完整的示例。将两个网格放到一个表单上并复制代码下面跟随。

To help, a method that returns a List<Foo> where there are a random number of Foo items in each list.为了提供帮助,一个返回List<Foo>的方法,其中每个列表中有随机数量的Foo项。 This method may look something like below with the global rand Random variable to get a random number of Foo items to add to the list in addition to setting a random Value for each Foo object.除了为每个Foo object 设置随机Value之外,此方法可能类似于下面的全局rand Random变量,以获取要添加到列表中的随机数Foo项。

 Random rand = new Random();

private List<Foo> GettRandomNumberOfFooList() {
  int numberOfFoo = rand.Next(2, 20);
  List<Foo> fooList = new List<Foo>();
  for (int i = 0; i < numberOfFoo; i++) {
    fooList.Add(new Foo { Id = i, Value = rand.Next(1, 100).ToString() });
  }
  return fooList;
}

We can use this method to create a List<FooList> for testing.我们可以使用此方法创建一个List<FooList>进行测试。 The master grids DataSource will be this list.主网格DataSource将是这个列表。 Then, to determine which list to display in the details grid, we will simply use the selected FooList.TheFooList property.然后,要确定在详细信息网格中显示哪个列表,我们将简单地使用 selected FooList.TheFooList属性。

Next, we need a trigger to know when to “change” the details data source.接下来,我们需要一个触发器来知道何时“更改”详细信息数据源。 In this case I used the grids, RowEnter method to change the details grids data source.在这种情况下,我使用网格, RowEnter方法来更改详细信息网格数据源。

Below is the code described above.下面是上面描述的代码。 The master grid will have 15 FooList items.主网格将有 15 个FooList项。

List<FooList> FooLists;

public Form1() {
  InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e) {
  FooLists = new List<FooList>();
  for (int i = 0; i < 15; i++) {
    FooLists.Add(new FooList { ListName = "Foo List " + (i + 1), TheFooList = GettRandomNumberOfFooList() });
  }
  dataGridView1.DataSource = FooLists;
  dataGridView2.DataSource = FooLists[0].TheFooList;
}


private void dataGridView1_RowEnter(object sender, DataGridViewCellEventArgs e) {
  dataGridView2.DataSource = FooLists[e.RowIndex].TheFooList;
}

This should produce something like...这应该产生类似...

在此处输入图像描述

Lastly, this is just an example and using a BindingList/BindingSource may make things easier.最后,这只是一个示例,使用BindingList/BindingSource可能会使事情变得更容易。 This is a very simple example of using a “Master-Detail” approach with a List of Lists.这是一个非常简单的示例,它使用“主从”方法和列表列表。

Implementing IListSource and mapping to DataTabe internally在内部实现 IListSource 并映射到 DataTabe

You can create a custom data source which implements IListSource and set it as data source of DataGridView.您可以创建一个实现IListSource的自定义数据源并将其设置为 DataGridView 的数据源。 To implement the interface properly to satisfy your requirement:要正确实现接口以满足您的要求:

  • In constructor, accept original list and map it to a DataTable .在构造函数中,接受原始列表并将 map 接收到DataTable
  • Subscribe to ListChanged event of the DefaultView property of you data table and apply changes to your original list.订阅数据表的DefaultView属性的ListChanged事件并将更改应用于原始列表。
  • For GetList method, return the mapped data table.对于GetList方法,返回映射的数据表。

Then when you bind DataGridView to your new data source, all the editing operations will immediately reflect in your original list:然后,当您将 DataGridView 绑定到新数据源时,所有编辑操作都会立即反映在您的原始列表中:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

ListListDataSource Implementation ListListDataSource 实现

public class ListListDataSource<T> : IListSource
{
    List<List<T>> data;
    DataTable table;
    public ListListDataSource(List<List<T>> list)
    {
        this.data = list;
        table = new DataTable();
        for (int i = 0; i < list.First().Count(); i++)
        {
            TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
                .Where(p => p.IsBrowsable).ToList().ForEach(p =>
                {
                    if (p.IsBrowsable)
                    {
                        var c = new DataColumn($"[{i}].{p.Name}", p.PropertyType);
                        c.ReadOnly = p.IsReadOnly;
                        table.Columns.Add(c);
                    }
                });
        }
        foreach (var innerList in list)
        {
            table.Rows.Add(innerList.SelectMany(
                x => TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>()
                .Where(p => p.IsBrowsable).Select(p => p.GetValue(x))).ToArray());
        }
        table.DefaultView.AllowDelete = false;
        table.DefaultView.AllowNew = false;
        table.DefaultView.ListChanged += DefaultView_ListChanged;
    }

    public bool ContainsListCollection => false;
    public IList GetList()
    {
        return table.DefaultView;
    }
    private void DefaultView_ListChanged(object sender, ListChangedEventArgs e)
    {
        if (e.ListChangedType != ListChangedType.ItemChanged)
            throw new NotSupportedException();
        var match = Regex.Match(e.PropertyDescriptor.Name, @"\[(\d+)\]\.(\w+)");
        var index = int.Parse(match.Groups[1].Value);
        var propertyName = match.Groups[2].Value;
        typeof(T).GetProperty(propertyName).SetValue(data[e.NewIndex][index],
            table.Rows[e.NewIndex][e.PropertyDescriptor.Name]);
    }
}

Then bind your list to DataGridView like this:然后将您的列表绑定到DataGridView ,如下所示:

List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
    foos = new List<List<Foo>>{
        new List<Foo>(){
            new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
        },
        new List<Foo>() {
            new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
        },
    };
    dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}

在此处输入图像描述

And when you edit data in DataGridView, in fact you are editing the original list.而当您在 DataGridView 中编辑数据时,实际上您是在编辑原始列表。

Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:此外,如果您想隐藏某个属性,只需将[Browsable(false)]添加到该属性即可:

public class Foo
{
    [Browsable(false)]
    public int Id { get; set; }
    public string Value { get; set; }
}

在此处输入图像描述

Using Custom TypeDescriptor使用自定义类型描述符

An interesting approach is creating a new data source using a custom TypeDescriptor .一种有趣的方法是使用自定义TypeDescriptor创建新数据源。

Type descriptor provide information about type, including list of properties and getting and setting property values.类型描述符提供有关类型的信息,包括属性列表以及获取和设置属性值。 DataTable also works the same way, to show list of columns in DataGridView, it returns a list of property descriptors containing properties per column. DataTable 也以同样的方式工作,为了在 DataGridView 中显示列列表,它返回包含每列属性的属性描述符列表。

Then when you bind DataGridView to your new data source, you are in fact editing the original list:然后,当您将 DataGridView 绑定到新数据源时,您实际上是在编辑原始列表:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

ListListDataSource implementation using TypeDescriptor使用 TypeDescriptor 实现 ListListDataSource

Here I've created a custom type descriptor for each inner list to treat is as a single object having a few properties.在这里,我为每个内部列表创建了一个自定义类型描述符,以将其视为具有一些属性的单个 object。 The properties are all properties of each element of the inner list and I've created a property descriptor for properties:这些属性是内部列表中每个元素的所有属性,我为属性创建了一个属性描述符:

public class ListListDataSource<T> : List<FlatList>
{
    public ListListDataSource(List<List<T>> list)
    {
        this.AddRange(list.Select(x => 
            new FlatList(x.Cast<object>().ToList(), typeof(T))));
    }
}
public class FlatList : CustomTypeDescriptor
{
    private List<object> data;
    private Type type;
    public FlatList(List<object> data, Type type)
    {
        this.data = data;
        this.type = type;
    }
    public override PropertyDescriptorCollection GetProperties()
    {
        return this.GetProperties(new Attribute[] { });
    }
    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        var properties = new List<PropertyDescriptor>();
        for (int i = 0; i < data.Count; i++)
        {
            foreach (PropertyDescriptor p in TypeDescriptor.GetProperties(type))
                properties.Add(new FlatListProperty(i, p));
        }
        return new PropertyDescriptorCollection(properties.ToArray());
    }
    public object this[int i]
    {
        get => data[i];
        set => data[i] = value;
    }
}
public class FlatListProperty : PropertyDescriptor
{
    int index;
    PropertyDescriptor originalProperty;
    public FlatListProperty(int index, PropertyDescriptor originalProperty)
        : base($"[{index}].{originalProperty.Name}",
                originalProperty.Attributes.Cast<Attribute>().ToArray())
    {
        this.index = index;
        this.originalProperty = originalProperty;
    }
    public override Type ComponentType => typeof(FlatList);
    public override bool IsReadOnly => false;
    public override Type PropertyType => originalProperty.PropertyType;
    public override bool CanResetValue(object component) => false;
    public override object GetValue(object component) =>
        originalProperty.GetValue(((FlatList)component)[index]);
    public override void ResetValue(object component) { }
    public override void SetValue(object component, object value) =>
        originalProperty.SetValue(((FlatList)component)[index], value);
    public override bool ShouldSerializeValue(object component) => true;
}

To bind data:绑定数据:

List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
    foos = new List<List<Foo>>{
        new List<Foo>(){
            new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
        },
        new List<Foo>() {
            new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
        },
    };
    dataGridView1.DataSource = new ListListDataSource<Foo>(foos);
}

在此处输入图像描述

And when you edit data in DataGridView, in fact you are editing the original list.而当您在 DataGridView 中编辑数据时,实际上您是在编辑原始列表。

Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:此外,如果您想隐藏某个属性,只需将[Browsable(false)]添加到该属性即可:

public class Foo
{
    [Browsable(false)]
    public int Id { get; set; }
    public string Value { get; set; }
}

在此处输入图像描述

Flattening the List<List<T>> into a List<T>将 List<List<T>> 展平为 List<T>

If showing data in a flattened structure for editing is acceptable, then you can use:如果可以接受以扁平结构显示数据以进行编辑,则可以使用:

List<List<Foo>> foos;
private void Form1_Load(object sender, EventArgs e)
{
    foos = new List<List<Foo>>{
            new List<Foo>(){
                new Foo() { Id=11, Value="11"}, new Foo() { Id = 12, Value = "12" }
            },
            new List<Foo>() {
                new Foo() { Id=21, Value="21"}, new Foo() { Id = 22, Value = "22" }
            },
        };
    dataGridView1.DataSource = foos.SelectMany(x=>x).ToList();
}

And edit data in a flat list, like this:并在平面列表中编辑数据,如下所示:

在此处输入图像描述

When you edit each row, you are in fact editing the original list.当您编辑每一行时,您实际上是在编辑原始列表。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM