Winforms ListBox with DataSource bound to dotted path of nested object crashes when intermediate object of dotted path is null

huangapple go评论62阅读模式
英文:

Winforms ListBox with DataSource bound to dotted path of nested object crashes when intermediate object of dotted path is null

问题

  • ListBox的DataSource绑定到Detail.Tags
  • 当我选择第一行时,ListBox会按预期填充。
  • 当我选择第二行时,期望的(并且希望的)结果是,ListBox简单地显示为空,因为为了演示目的,ItemB的Detail属性被故意设置为null,所以ItemB的Detail.Tags不存在。
  • 实际结果是程序崩溃到桌面,出现System.ArgumentException: '复杂数据绑定接受的数据源可以是IList或IListSource。'
英文:
  • The ListBox's DataSource is bound to Detail.Tags.
  • When I select the first row, ListBox populates as expected.
  • When I select the second row, the expected (and desired) result is that the ListBox simply displays nothing, because ItemB's Detail property is purposely null for demonstration purposes, so ItemB's Detail.Tags doesn't exist.
  • Actual result is that program crashes to desktop with System.ArgumentException: 'Complex DataBinding accepts as a data source either an IList or an IListSource.'

Winforms ListBox with DataSource bound to dotted path of nested object crashes when intermediate object of dotted path is null

Minimal reproducible example:

    public partial class Form1 : Form
    {
        private readonly IList<Item> _items;
        private BindingSource _bs = new BindingSource(){ DataSource = typeof(Item) };


        public Form1()
        {
            InitializeComponent();
            _items = GenerateSampleItems();
        }


        private void Form1_Load(object sender, EventArgs e)
        {
            dataGridView1.DataSource = _items;
            listBox1.DataBindings.Add(new Binding("DataSource", _bs, "Detail.Tags", false, DataSourceUpdateMode.Never));
        }
        

        private void DataGridView1_SelectionChanged(object sender, EventArgs e)
        {
            if (dataGridView1.SelectedRows.Count == 1)
            {
                _bs.DataSource = dataGridView1.SelectedRows[0].DataBoundItem;
            }
            else
            {
                _bs.DataSource = typeof(Item);
            }
        }


        private IList<Item> GenerateSampleItems()
        {
            return new List<Item>()
            {
                new Item()
                {
                    Name = "ItemA"
                    ,Detail = new Detail()
                    {
                         Expiration = new DateTime(2024,1,1)
                        ,Tags       = new BindingList<Tag>(new List<Tag>()
                                                          {
                                                              new Tag() 
                                                              { 
                                                                   TagName = "FirstT"
                                                                  ,TagValue = "FirstV"
                                                              }
                                                              ,new Tag() 
                                                              { 
                                                                   TagName = "SecondT"
                                                                  ,TagValue = "SecondV"
                                                              }
                                                          })
                    }
                }
                ,new Item()
                {
                    Name = "ItemB"
                    // Detail purposely omitted
                }
                ,new Item()
                {
                    Name = "ItemC"
                    // Detail purposely omitted
                }

            };
        }
    }


    class Item
    {
        public string Name              { get; set; }
        public Detail Detail            { get; set; }
    }

    public class Detail
    {
        public DateTime Expiration      { get; set; }
        public BindingList<Tag> Tags    { get; set; }      
    }

    public class Tag
    {
        public string TagName           { get; set; }
        public string TagValue          { get; set; }
    }

答案1

得分: 1

你可以通过为每个模型创建一个 BindingSource 来解决这个问题:

  1. BindingSource,其中 DataSource 属性设置为一个 Item 列表。这个是 DataGridView.DataSource
  2. 第二个 BindingSource 用于导航主 BindingSourceDetail 数据成员。
  3. 第三个用于导航和显示详细的 BindingSourceTags 数据成员。这个是 ListBox.DataSource
public partial class Form1 : Form
{
    private IList<Item> _items;
    private BindingSource _bsItems, _bsDetail, _bsTags;

    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _items = GenerateSampleItems();
        _bsItems = new BindingSource(_items, null);
        _bsDetail = new BindingSource(_bsItems, "Detail");
        _bsTags = new BindingSource(_bsDetail, "Tags");
        dataGridView1.DataSource = _bsItems;
        listBox1.DataSource = _bsTags;
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        _bsItems.Dispose();
        _bsDetail.Dispose();
        _bsTags.Dispose();
    }

    private IList<Item> GenerateSampleItems()
    {
        return new List<Item>()
        {
            new Item()
            {
                Name = "ItemA",
                Detail = new Detail
                {
                    Expiration = new DateTime(2024, 1, 1),
                    Tags = new BindingList<Tag>(new List<Tag>()
                    {
                        new Tag
                        {
                            TagName = "FirstT",
                            TagValue = "FirstV"
                        },
                        new Tag
                        {
                            TagName = "SecondT",
                            TagValue = "SecondV"
                        }
                    })
                }
            },
            new Item()
            {
                Name = "ItemB"
                // 详细信息故意省略
            },
            new Item()
            {
                Name = "ItemC"
                // 详细信息故意省略
            }
        };
    }
}

// 项目的命名空间中的其他位置
public class Item
{
    public string Name { get; set; }
    public Detail Detail { get; set; }

    // 可选: 根据需要更改或删除...
    public override string ToString()
    {
        return $"Name: {Name} - Detail: {Detail}";
    }
}

public class Detail
{
    public DateTime Expiration { get; set; }
    public BindingList<Tag> Tags { get; set; }

    // 可选: 根据需要更改或删除...
    public override string ToString()
    {
        var tags = $"[{string.Join(", ", Tags)}]";
        return $"Expiration: {Expiration} - Tags: {tags}";
    }
}

public class Tag
{
    public string TagName { get; set; }
    public string TagValue { get; set; }

    // 可选: 根据需要更改或删除...
    public override string ToString()
    {
        return $"{TagName}: {TagValue}";
    }
}

就是这样。无需添加 DataBindings 或处理网格的 SelectionChanged 事件,如您在代码片段中所示。


另一方面,如果您需要显示选定的 Item.Detail.Tags,则需要在网格的选择更改时将它们展开为列表,并将结果绑定到 ListBox

// +
using System.Linq;

public partial class Form1 : Form
{
    private BindingSource _bsItems;

    public Form1() => InitializeComponent();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _bsItems = new BindingSource(GenerateSampleItems(), null);
        dataGridView1.DataSource = _bsItems;
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        _bsItems.Dispose();
    }

    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
        listBox1.DataSource = dataGridView1.SelectedCells
            .Cast<DataGridViewCell>()
            .Select(cell => cell.OwningRow).Distinct()
            .Where(row => (row.DataBoundItem as Item)?.Detail != null)
            .SelectMany(row => (row.DataBoundItem as Item).Detail.Tags)
            .ToList();
    }
}
英文:

You can solve this problem by Creating a BindingSource for each model:

  1. Main BindingSource where its DataSource property is set to a list of Item. This one is the DataGridView.DataSource.
  2. Second BindingSource to navigate the Detail data members of the main BindingSource.
  3. Third one to navigate and display the Tags data members of the detail's BindingSource. This one is the ListBox.DataSource.
public partial class Form1 : Form
{
    private IList&lt;Item&gt; _items;
    private BindingSource _bsItems, _bsDetail, _bsTags;

    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _items = GenerateSampleItems();
        _bsItems = new BindingSource(_items, null);
        _bsDetail = new BindingSource(_bsItems, &quot;Detail&quot;);
        _bsTags = new BindingSource(_bsDetail, &quot;Tags&quot;);
        dataGridView1.DataSource = _bsItems;
        listBox1.DataSource = _bsTags;
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        _bsItems.Dispose();
        _bsDetail.Dispose();
        _bsTags.Dispose();
    }

    private IList&lt;Item&gt; GenerateSampleItems()
    {
        return new List&lt;Item&gt;()
        {
            new Item()
            {
                Name = &quot;ItemA&quot;,
                Detail = new Detail
                {
                    Expiration = new DateTime(2024,1,1),
                    Tags = new BindingList&lt;Tag&gt;(new List&lt;Tag&gt;()
                    {
                        new Tag
                        {
                            TagName = &quot;FirstT&quot;,
                            TagValue = &quot;FirstV&quot;
                        },
                        new Tag
                        {
                            TagName = &quot;SecondT&quot;,
                            TagValue = &quot;SecondV&quot;
                        }
                    })
                }
            },
            new Item()
            {
                Name = &quot;ItemB&quot;
                // Detail purposely omitted
            },
            new Item()
            {
                Name = &quot;ItemC&quot;
                // Detail purposely omitted
            }
        };
    }
}

// Elsewhere within the project&#39;s namespace
public class Item
{
    public string Name { get; set; }
    public Detail Detail { get; set; }

    // Optional: Change, remove as needed...
    public override string ToString()
    {
        return $&quot;Name: {Name} - Detail: {Detail}&quot;;
    }
}

public class Detail
{
    public DateTime Expiration { get; set; }
    public BindingList&lt;Tag&gt; Tags { get; set; }

    // Optional: Change, remove as needed...
    public override string ToString()
    {
        var tags = $&quot;[{string.Join(&quot;, &quot;, Tags)}]&quot;;
        return $&quot;Expiration: {Expiration} - Tags: {tags}&quot;;
    }
}

public class Tag
{
    public string TagName { get; set; }
    public string TagValue { get; set; }

    // Optional: Change, remove as needed...
    public override string ToString()
    {
        return $&quot;{TagName}: {TagValue}&quot;;
    }
}

That's all. No need to add DataBindings nor to handle the grid's SelectionChanged event as shown in your code snippet.


On the other hand, if you need to display the selected Item.Detail.Tags, then you need to flatten them in a list whenever the grid's selection changes and bind the result to the ListBox.

// +
using System.Linq;

public partial class Form1 : Form
{
    private BindingSource _bsItems;

    public Form1() =&gt; InitializeComponent();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _bsItems = new BindingSource(GenerateSampleItems(), null);
        dataGridView1.DataSource = _bsItems;
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        _bsItems.Dispose();
    }

    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
        listBox1.DataSource = dataGridView1.SelectedCells
            .Cast&lt;DataGridViewCell&gt;()
            .Select(cell =&gt; cell.OwningRow).Distinct()
            .Where(row =&gt; (row.DataBoundItem as Item)?.Detail != null)
            .SelectMany(row =&gt; (row.DataBoundItem as Item).Detail.Tags)
            .ToList();
    }
}

huangapple
  • 本文由 发表于 2023年2月14日 02:12:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/75439719.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定