Jonathan Wood
Jonathan Wood

Reputation: 67315

Does BindingList<T> benefit from BindingSource or INotifyPropertyChanged?

I have a WinForms form that defines a BindingList<Item>. And this list is assigned to the DataSource property of a ListBox.

This works pretty well. Any changes made to the list are also reflected in the ListBox. (My actual code is below.)

But looking at examples online, I see many of them also incorporate BindingSource and/or INotifyPropertyChanged. Can anyone tell me if I need either of these and whether my code would benefit from either of them?

public partial class Form1 : Form
{
    BindingList<Item> Items { get; set; }

    public Form1()
    {
        InitializeComponent();
        Items =
        [
            new("First", 1, DateTime.Now),
            new("Second", 2, DateTime.Now),
            new("Third", 3, DateTime.Now),
            new("Fourth", 4, DateTime.Now),
            new("Fifth", 5, DateTime.Now),
            new("Sixth", 6, DateTime.Now),
            new("Seventh", 7, DateTime.Now),
            new("Eighth", 8, DateTime.Now),
            new("Nineth", 9, DateTime.Now),
            new("Tenth", 10, DateTime.Now),
        ];
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        listBox1.DataSource = Items;
    }

    private void AddButton_Click(object sender, EventArgs e)
    {
        EditForm form = new();
        if (form.ShowDialog() == DialogResult.OK && form.Item != null)
        {
            int index = listBox1.SelectedIndex;
            if (index < 0)
                index = 0;
            Items.Insert(index, form.Item);
            listBox1.SelectedIndex = index;
        }
        VerifyList();
    }

    private void EditButton_Click(object sender, EventArgs e)
    {
        int index = listBox1.SelectedIndex;
        if (index >= 0)
        {
            EditForm form = new() { Item = Items[index] };
            if (form.ShowDialog() == DialogResult.OK && form.Item != null)
                Items[index] = form.Item;
        }
        VerifyList();
    }

    private void DeleteButton_Click(object sender, EventArgs e)
    {
        int index = listBox1.SelectedIndex;
        if (index >= 0)
            Items.RemoveAt(index);
        VerifyList();
    }
}

Upvotes: -1

Views: 84

Answers (1)

IV.
IV.

Reputation: 9438

I'd like to focus on the INotifyPropertyChanged aspect of your question, because this is what enables two-way binding. The distinction is that BindingList<T> is not only capable of notifying when the list itself changes, it is also listening for property changes of the items it contains (provided that data model implements INP). Here's what I mean:


First, you need an Item class that is itself bindable:

class Item : INotifyPropertyChanged
{
    static int _autoIncrement = 1;
    public int Id { get; init; } = _autoIncrement++;
    public string Name
    {
        get => _name;
        set
        {
            if (!Equals(_name, value))
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }
    string _name = string.Empty;

    public decimal Price
    {
        get => _price;
        set
        {
            if (!Equals(_price, value))
            {
                _price = value;
                OnPropertyChanged();
            }
        }
    }
    decimal _price = default;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    public event PropertyChangedEventHandler? PropertyChanged;
}

Two Way Binding for Programmatic Updates

You said you're using ListBox but I'm taking the liberty of using DataGridView because it's very easy to see what I mean when I say this. And in this minimal example, the purpose of the test button is to change the item properties programmatically in hopes of seeing the change reflected in the "arbitrary collection view" that contains the item. Without the INotifyPropertyChanged this update doesn't occur.

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        buttonTest.Click += TestTwoWayBinding;
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        dataGridView.DataSource = Items;
        dataGridView
            .Columns[nameof(Item.Name)]
            .AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
        
        // BONUS: Subscribe to the ListChanged event to see item changes.
        Items.ListChanged += (sender, e) =>
        {
            switch (e.ListChangedType)
            {
                case ListChangedType.ItemChanged:
                    if( sender is IList<Item> list &&
                        e.NewIndex < list.Count)
                    {
                        var item = list[e.NewIndex];
                        var propertyName = e.PropertyDescriptor.Name;
                        var propertyValue = e.PropertyDescriptor.GetValue(item); 
                        Text = $"{propertyName}={propertyValue}";
                    }
                    break;
            }
        };
    }
    BindingList<Item> Items = new BindingList<Item>
    {
        new Item { Name = "Meat", Price = 0.99m },
        new Item { Name = "Cheese", Price = 0.59m },
        new Item { Name = "Eggs", Price = 2.99m },
    };
    private void TestTwoWayBinding(object? sender, EventArgs e)
    {
        if(Items.FirstOrDefault(_=>_.Name == "Eggs") is { } item)
        {
            item.Price *= 2;
        }
    }
}

two-way binding works


As for BindingSource, I've "personally" have used this alternative when I needed gain some finer-grained control over how a property change maps to its displayed version, e.g. when I've got some kind of data template. But (for me) the BindingList<T> is usually a great choice because of the notifications of the items it contains (and in this respect it's different from, say, ObservableCollection<T>).

Upvotes: 3

Related Questions