简体   繁体   中英

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?

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

By Multidim I mean something to the affect of:

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.

The problem you describe can be solved a couple of different ways. One is to “flatten” each List<Foo>. Basically this will flatten ALL the Foo items in a list into a “single” string. With this approach as I commented, you would end up with one column and each row would be a “flattened” List<Foo>. Each cell may have a different number of Foo items in the string.

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.

An example may work best to show what I mean. First, we need to make an additional class. Reason being that is if we use a List<List<Foo>> as a DataSource to the master grid, it will show something like…

在此处输入图像描述

As shown the two columns are going to be the List “Capacity” and “Count.” This may work; however, it may be confusing to the user. That is why we want this other class. It is a simple “wrapper” around the List<Foo> and to display this we will add a “Name” property to this class. This will be displayed in the master grid.

Given the current modified Foo class…

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

This FooList class may look something like…

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

A List<FooList> would display something like…

在此处输入图像描述

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.

To help, a method that returns a List<Foo> where there are a random number of Foo items in each list. 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.

 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. The master grids DataSource will be this list. Then, to determine which list to display in the details grid, we will simply use the selected FooList.TheFooList property.

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.

Below is the code described above. The master grid will have 15 FooList items.

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. This is a very simple example of using a “Master-Detail” approach with a List of Lists.

Implementing IListSource and mapping to DataTabe internally

You can create a custom data source which implements IListSource and set it as data source of DataGridView. To implement the interface properly to satisfy your requirement:

  • In constructor, accept original list and map it to a DataTable .
  • Subscribe to ListChanged event of the DefaultView property of you data table and apply changes to your original list.
  • For GetList method, return the mapped data table.

Then when you bind DataGridView to your new data source, all the editing operations will immediately reflect in your original list:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

ListListDataSource Implementation

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:

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.

Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:

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 .

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.

Then when you bind DataGridView to your new data source, you are in fact editing the original list:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

ListListDataSource implementation using TypeDescriptor

Here I've created a custom type descriptor for each inner list to treat is as a single object having a few properties. 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.

Also if you want to hide a property, it's as easy as adding [Browsable(false)] to the property:

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

在此处输入图像描述

Flattening the List<List<T>> into a 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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