英文:
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 
DataSourceis bound toDetail.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 
Detailproperty is purposely null for demonstration purposes, so ItemB'sDetail.Tagsdoesn'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.' 
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 来解决这个问题:
- 主 
BindingSource,其中DataSource属性设置为一个Item列表。这个是DataGridView.DataSource。 - 第二个 
BindingSource用于导航主BindingSource的Detail数据成员。 - 第三个用于导航和显示详细的 
BindingSource的Tags数据成员。这个是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:
- Main 
BindingSourcewhere itsDataSourceproperty is set to a list ofItem. This one is theDataGridView.DataSource. - Second 
BindingSourceto navigate theDetaildata members of the mainBindingSource. - Third one to navigate and display the 
Tagsdata members of the detail'sBindingSource. This one is theListBox.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"
                // Detail purposely omitted
            },
            new Item()
            {
                Name = "ItemC"
                // Detail purposely omitted
            }
        };
    }
}
// Elsewhere within the project'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 $"Name: {Name} - Detail: {Detail}";
    }
}
public class Detail
{
    public DateTime Expiration { get; set; }
    public BindingList<Tag> Tags { get; set; }
    // Optional: Change, remove as needed...
    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; }
    // Optional: Change, remove as needed...
    public override string ToString()
    {
        return $"{TagName}: {TagValue}";
    }
}
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() => 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();
    }
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。



评论