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.
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:
DataTable
.ListChanged
event of the DefaultView
property of you data table and apply changes to your original list.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; }
}
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; }
}
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.