【问题标题】:DataGridView Databinding to List<List<T>>DataGridView 数据绑定到 List<List<T>>
【发布时间】:2021-01-27 19:05:40
【问题描述】:

给定代码

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

有没有办法将 Multidim ICollection 绑定到属性 Value 上的 DataGridView,当您更改单元格时,对象的 Value 属性会更新?

在这种情况下,列表中的每个 Foo 实例将代表 DataGridView 中的一个单元格,并且行/列将被保留,就像它们在 multidim ICollection 中一样

我所说的 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]
]

嵌套列表中的每个元素实际上是 Foo 的实例。

【问题讨论】:

  • 用 SelectMany 展平: fooList.SelectMany(x => x).ToList();
  • @jdweng 会保留多暗阵列的行/列吗?
  • 你说的multidim array是什么意思? Rank > 1 的数组,例如 Foo[,]? Foo 数组的数组,例如 Foo[][],或者,如您所展示的,列表列表(看不到数组)?
  • 考虑这个...因为你想将List&lt;List&lt;Foo&gt;&gt;绑定到网格,那么网格中的每一列都是List&lt;Foo&gt;,因为每个List&lt;Foo&gt;可能有不同数量的@987654328 @s 在每个列表中,然后每列可以有不同的行数。即使您“展平”每个 List&lt;Foo&gt; 列表,使用 List&lt;List&lt;Foo&gt;&gt; 也只会以一 (1) 列结束,这样每一行在每个单元格中都有不同数量的 Foo 项目。
  • 你真的应该用winformsdatagridview 标记你的问题。因为有datagridview 的标签,所以很有可能有人是这方面的专家。

标签: c# .net winforms data-binding datagridview


【解决方案1】:

在内部实现 IListSource 并映射到 DataTabe

您可以创建一个实现IListSource 的自定义数据源并将其设置为DataGridView 的数据源。正确实现接口以满足您的要求:

  • 在构造函数中,接受原始列表并将其映射到DataTable
  • 订阅您数据表的DefaultView 属性的ListChanged 事件并将更改应用到您的原始列表。
  • 对于GetList方法,返回映射的数据表。

那么当你将 DataGridView 绑定到你的新数据源时,所有的编辑操作都会立即反映在你原来的列表中:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

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]);
    }
}

然后将您的列表绑定到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);
}

而当你在 DataGridView 中编辑数据时,其实你是在编辑原始列表。

另外,如果您想隐藏某个属性,只需将[Browsable(false)] 添加到该属性即可:

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

【讨论】:

  • 认为这可能正是我所需要的。我还没有机会测试,但我会告诉你的。感谢大家的宝贵时间。
  • 不要错过我的其他答案。
  • 会的。我将研究哪个可能是给定用例的最佳选择。
  • 我也会编辑这个。我已经把它变成了一个通用的解决方案,所以你可以将它用于 ant 类型的 List>,而不仅仅是 Foo。
  • 好吧,我测试了两个答案,它们都有效。它们独立于您的模型,您可以使用它们在 DataGridView 中编辑任何List&lt;List&lt;T&gt;&gt;
【解决方案2】:

您描述的问题可以通过几种不同的方式解决。一种是“展平”每个List&lt;Foo&gt;. 基本上这会将列表中的所有Foo 项目展平为“单个”string. 正如我评论的那样,使用这种方法,您最终会得到一列,每一行将是“扁平化”List&lt;Foo&gt;. 每个单元格在字符串中可能有不同数量的Foo 项。

在这种情况下和其他情况下,这可能不是预期的结果。由于您有一个 ListLists,那么使用两 (2) 个网格的“主从”方法可能会使事情变得更容易。在这种方法中,第一个网格(主)将有一个 (1) 列,每个行将是List&lt;Foo&gt;. 因为我们已经知道网格不会将此列表显示到单个单元格中并且我们不想“展平”列表,所以这就是第二个(详细)网格发挥作用的地方。第一个网格显示Foo的所有列表,无论选择哪个“行”,第二个网格(详细)将显示所有List&lt;Foo&gt;项目。

一个例子可能最能说明我的意思。首先,我们需要创建一个额外的类。原因是如果我们将List&lt;List&lt;Foo&gt;&gt; 用作主网格的DataSource,它将显示类似...

如图所示,这两列将是List“容量”和“计数”。这可能有效;但是,它可能会使用户感到困惑。这就是我们想要这个其他类的原因。它是List&lt;Foo&gt; 的简单“包装器”,为了显示它,我们将向此类添加“名称”属性。这将显示在主网格中。

鉴于当前修改的Foo 类……

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

这个FooList 类可能看起来像……

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

List&lt;FooList&gt; 会显示类似...

现在,当用户在第一个“主”网格中“选择”一行时,第二个“详细”网格将显示该列表中的所有 Foo 项目。下面是一个完整示例。将两个网格放到一个形成并复制下面的代码以跟随。

为了提供帮助,一个返回 List&lt;Foo&gt; 的方法,其中每个列表中有随机数量的 Foo 项目。除了为每个 Foo 对象设置随机 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;
}

我们可以使用这种方法创建一个List&lt;FooList&gt; 进行测试。主网格DataSource 将是这个列表。然后,要确定在详细信息网格中显示哪个列表,我们将简单地使用选中的FooList.TheFooList 属性。

接下来,我们需要一个触发器来知道何时“更改”详细信息数据源。在这种情况下,我使用了网格,RowEnter 方法来更改详细信息网格数据源。

下面是上面描述的代码。主网格将有 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;
}

这应该会产生类似...

最后,这只是一个示例,使用BindingList/BindingSource 可能会使事情变得更容易。这是一个非常简单的示例,使用“主从”方法和列表列表。

【讨论】:

  • 同意这个用例正是BindingSource 让事情变得如此简单的地方。我会说这是理想的,但这是可行的。
【解决方案3】:

使用自定义类型描述符

一种有趣的方法是使用自定义 TypeDescriptor 创建新数据源。

类型描述符提供关于类型的信息,包括属性列表以及获取和设置属性值。 DataTable 也以同样的方式工作,为了在 DataGridView 中显示列列表,它返回一个包含每列属性的属性描述符列表。

那么当你将 DataGridView 绑定到你的新数据源时,你实际上是在编辑原始列表:

dataGridView1.DataSource = new FooDataSource(yourListOfListOfFoo);

ListListDataSource 使用 TypeDescriptor 实现

在这里,我为每个内部列表创建了一个自定义类型描述符,以将其视为具有几个属性的单个对象。属性是内部列表中每个元素的所有属性,我为属性创建了一个属性描述符:

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;
}

绑定数据:

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);
}

而当你在 DataGridView 中编辑数据时,其实你是在编辑原始列表。

此外,如果您想隐藏某个属性,只需将[Browsable(false)] 添加到该属性即可:

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

【讨论】:

    【解决方案4】:

    将 List> 扁平化为 List

    如果可以接受以扁平结构显示数据以进行编辑,则可以使用:

    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();
    }
    

    并在平面列表中编辑数据,如下所示:

    编辑每一行时,实际上是在编辑原始列表。

    【讨论】:

    • 最后,如果可以接受以扁平结构显示数据进行编辑,您可以使用它。
    猜你喜欢
    • 2010-09-12
    • 2013-10-24
    • 2012-12-11
    • 1970-01-01
    • 2013-05-17
    • 2017-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多