NewRow with DataGridView bound to list of objects

mafrosis

Well-known member
Joined
Jun 5, 2006
Messages
88
Location
UK
Programming Experience
5-10
Hey all,

When you bind a DataGridView to a DataTable it is easy to programmatically add rows - by just adding to the bound DataSource (which in this case is a DataTable).

Ive been binding typed lists of objects to my DataGridView. This works fine, but I am unable to display new rows in the DataGridView when I add additional objects to the bound list..

Any ideas? Thanks for any ideas/explanation. Im guessing this just cant be done..

mafro

The code that follows is a working example.

VB.NET:
[COLOR="Green"]'in my form[/COLOR]
[COLOR="Blue"]Dim [/COLOR]items [COLOR="Blue"]As New[/COLOR] List([COLOR="Blue"]Of [/COLOR]DemoObj)
items.Add([COLOR="Blue"]New[/COLOR] DemoObj(1, "some data"))
dgv.DataSource = items


[COLOR="Green"]'the object being bound[/COLOR]
[COLOR="Blue"]Class [/COLOR]DemoObj
	[COLOR="Blue"]Private [/COLOR]_pk [COLOR="Blue"]As [/COLOR]Int32
	[COLOR="Blue"]Private [/COLOR]_data [COLOR="Blue"]As String[/COLOR]
	
	[COLOR="Blue"]Public Property[/COLOR] pk [COLOR="Blue"]As Int32[/COLOR]
		[COLOR="Blue"]Get[/COLOR]
			[COLOR="Blue"]Return [/COLOR]_pk
		[COLOR="Blue"]End Get
		Set[/COLOR]([COLOR="Blue"]ByVal [/COLOR]value [COLOR="Blue"]As Int32[/COLOR])
			_pk = value
		[COLOR="Blue"]End Set
	End Property[/COLOR]
	
	[COLOR="Blue"]Public Property[/COLOR] data [COLOR="Blue"]As String[/COLOR]
		[COLOR="Blue"]Get[/COLOR]
			[COLOR="Blue"]Return [/COLOR]_data
		[COLOR="Blue"]End Get
		Set[/COLOR]([COLOR="Blue"]ByVal [/COLOR]value [COLOR="Blue"]As String[/COLOR])
			_data = value
		[COLOR="Blue"]End Set
	End Property[/COLOR]

	[COLOR="Blue"]Public Sub New[/COLOR](pk [COLOR="Blue"]As Int32[/COLOR], data [COLOR="Blue"]As String[/COLOR])
		_pk = pk
		_data = data
	[COLOR="Blue"]End Sub
End Class[/COLOR]
 
First the simple solutions:

1. Use a BindingList(Of DemoObj) instead of a List(Of DemoObj). Now when you change the list you call ResetBindings and any bound controls will be updated. If you only change an existing item then call ResetItem instead, so only one item gets updated.

2. Use a List(Of DemoObj) as you are, bind it to a BindingSource and bind that to the control. The BindingSource has ResetBindings, ResetItem and ResetCurrentItem methods to update the bound control(s) after a change.

Now to the more complex solution. The reason things are so easy with a bound DataTable is that DataView class, through which the DataTable's data is bound, implements the IBindingList interface. That interface exposes members to filter and sort the data, as well as add, edit and delete items and raise events to notify any bound controls of those changes. Rather than using a standard List or even BindingList, you can derive your own class from Collection(Of DemoObj) that implements IBindingList. Then your collection will be as easy to work with bound as a DataTable.

I looked at doing this myself some time ago but gave up because I thought that it was too complex for the perceived benefits. More recently it became more necessary so I revisited the subject. As it turns out it is really very easy to implement the IBindingList interface. There are quite a few members to implement but most take only a few lines of code and many only one. I even went a step further and implemented IBindingListView in many cases, which adds advanced sorting and filtering. If you don't need those then IBindingList is enough. As I said, most members are easy to implement, many ridiculously so. Sorting takes a bit more thought, particularly advanced sorting, i.e. sorting on multiple properties. Filtering is also somewhat complex and searching, which I have never implemented, is also. If you don't need those advanced features then it's really child's play. Here's an example of one of my business objects:
VB.NET:
Public Class TransactionEntry

#Region " Variables "

    Private _key As String
    Private _item As StockItem
    Private _description As String
    Private _price As Decimal
    Private _quantity As Double
    Private _extendedPrice As Decimal
    Private _salesRepNumber As String
    Private _taxable As Boolean
    Private _quantityRTD As Double

#End Region 'Variables

#Region " Properties "

    Public ReadOnly Property Key() As String
        Get
            Return Me._key
        End Get
    End Property

    Public ReadOnly Property Item() As StockItem
        Get
            Return Me._item
        End Get
    End Property

    Public Property Description() As String
        Get
            Return Me._description
        End Get
        Set(ByVal value As String)
            If Me._description <> value Then
                Me._description = value
                Me.OnDescriptionChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property Price() As Decimal
        Get
            Return Me._price
        End Get
        Set(ByVal value As Decimal)
            If Me._price <> value Then
                Me._price = value
                Me.OnPriceChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property Quantity() As Double
        Get
            Return Me._quantity
        End Get
        Set(ByVal value As Double)
            If Me._quantity <> value Then
                Me._quantity = value
                Me.OnQuantityChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property ExtendedPrice() As Decimal
        Get
            Return Me._extendedPrice
        End Get
        Set(ByVal value As Decimal)
            If Me._extendedPrice <> value Then
                Me._extendedPrice = value
                Me.OnExtendedPriceChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property SalesRepNumber() As String
        Get
            Return Me._salesRepNumber
        End Get
        Set(ByVal value As String)
            If Me._salesRepNumber <> value Then
                Me._salesRepNumber = value
                Me.OnSalesRepNumberChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property Taxable() As Boolean
        Get
            Return Me._taxable
        End Get
        Set(ByVal value As Boolean)
            If Me._taxable <> value Then
                Me._taxable = value
                Me.OnTaxableChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    Public Property QuantityRtd() As Double
        Get
            Return Me._quantityRTD
        End Get
        Set(ByVal value As Double)
            If Me._quantityRTD <> value Then
                Me._quantityRTD = value
                Me.OnQuantityRtdChanged(EventArgs.Empty)
            End If
        End Set
    End Property

#End Region 'Properties

#Region " Constructors "

    Public Sub New(ByVal entry As Communication.TransactionEntry)
        Me.Update(entry)
        'Me._key = entry.Key
        'Me._item = BusinessLogicManager.Instance.GetStockItem(entry.ItemID)
        'Me._description = entry.Description
        'Me._price = entry.Price
        'Me._quantity = entry.Quantity
        'Me._extendedPrice = entry.ExtendedPrice
        'Me._salesRepNumber = entry.SalesRepNumber
        'Me._taxable = entry.Taxable
        'Me._quantityRTD = entry.QuantityRtd
    End Sub

    'Public Sub New(ByVal key As String, _
    '               ByVal itemID As Integer, _
    '               ByVal description As String, _
    '               ByVal price As Decimal, _
    '               ByVal quantity As Double, _
    '               ByVal extendedPrice As Decimal, _
    '               ByVal salesRepNumber As String, _
    '               ByVal taxable As Boolean, _
    '               ByVal quantityRTD As Double)
    '    Me._key = key
    '    Me._item = BusinessLogicManager.Instance.GetStockItem(itemID)
    '    Me._description = description
    '    Me._price = price
    '    Me._quantity = quantity
    '    Me._extendedPrice = extendedPrice
    '    Me._salesRepNumber = salesRepNumber
    '    Me._taxable = taxable
    '    Me._quantityRTD = quantityRTD
    'End Sub

#End Region 'Constructors

#Region " Events "

    Public Event DescriptionChanged As EventHandler
    Public Event PriceChanged As EventHandler
    Public Event QuantityChanged As EventHandler
    Public Event ExtendedPriceChanged As EventHandler
    Public Event SalesRepNumberChanged As EventHandler
    Public Event TaxableChanged As EventHandler
    Public Event QuantityRtdChanged As EventHandler

#End Region 'Events

#Region " Methods "

    Public Sub Update(ByVal entry As Communication.TransactionEntry)
        Me._key = entry.Key
        Me._item = BusinessLogicManager.Instance.GetStockItem(entry.ItemID)
        Me._description = entry.Description
        Me._price = entry.Price
        Me._quantity = entry.Quantity
        Me._extendedPrice = entry.ExtendedPrice
        Me._salesRepNumber = entry.SalesRepNumber
        Me._taxable = entry.Taxable
        Me._quantityRTD = entry.QuantityRtd
    End Sub

    Protected Overridable Sub OnDescriptionChanged(ByVal e As EventArgs)
        RaiseEvent DescriptionChanged(Me, e)
    End Sub

    Protected Overridable Sub OnPriceChanged(ByVal e As EventArgs)
        RaiseEvent PriceChanged(Me, e)
    End Sub

    Protected Overridable Sub OnQuantityChanged(ByVal e As EventArgs)
        RaiseEvent QuantityChanged(Me, e)
    End Sub

    Protected Overridable Sub OnExtendedPriceChanged(ByVal e As EventArgs)
        RaiseEvent ExtendedPriceChanged(Me, e)
    End Sub

    Protected Overridable Sub OnSalesRepNumberChanged(ByVal e As EventArgs)
        RaiseEvent SalesRepNumberChanged(Me, e)
    End Sub

    Protected Overridable Sub OnTaxableChanged(ByVal e As EventArgs)
        RaiseEvent TaxableChanged(Me, e)
    End Sub

    Protected Overridable Sub OnQuantityRtdChanged(ByVal e As EventArgs)
        RaiseEvent QuantityRtdChanged(Me, e)
    End Sub

#End Region 'Methods

End Class
and here's the corresponding collection class that implements IBindingList:
VB.NET:
Public Class TransactionEntryCollection
    Inherits System.Collections.ObjectModel.Collection(Of TransactionEntry)
    Implements System.ComponentModel.IBindingList

#Region " Variables "

    Private entriesByKey As New Dictionary(Of String, TransactionEntry)

#End Region 'Variables

#Region " Event Handlers "

    Private Sub ItemPropertyChanged(ByVal sender As Object, ByVal e As EventArgs)
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemChanged, _
                                                  Me.Items.IndexOf(DirectCast(sender, _
                                                                   TransactionEntry))))
    End Sub

#End Region 'Event Handlers

#Region " Methods "

    Public Function FindByKey(ByVal key As String) As TransactionEntry
        Dim result As TransactionEntry = Nothing

        If Me.entriesByKey.ContainsKey(key) Then
            result = Me.entriesByKey(key)
        End If

        Return result
    End Function

    Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As TransactionEntry)
        AddHandler item.DescriptionChanged, AddressOf ItemPropertyChanged
        AddHandler item.ExtendedPriceChanged, AddressOf ItemPropertyChanged
        AddHandler item.PriceChanged, AddressOf ItemPropertyChanged
        AddHandler item.QuantityChanged, AddressOf ItemPropertyChanged
        AddHandler item.QuantityRtdChanged, AddressOf ItemPropertyChanged
        AddHandler item.SalesRepNumberChanged, AddressOf ItemPropertyChanged
        AddHandler item.TaxableChanged, AddressOf ItemPropertyChanged

        Me.entriesByKey.Add(item.Key, item)
        MyBase.InsertItem(index, item)
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemAdded, index))
    End Sub

    Protected Overrides Sub RemoveItem(ByVal index As Integer)
        Dim item As TransactionEntry = Me.Items(index)

        RemoveHandler item.DescriptionChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.ExtendedPriceChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.PriceChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.QuantityChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.QuantityRtdChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.SalesRepNumberChanged, AddressOf ItemPropertyChanged
        RemoveHandler item.TaxableChanged, AddressOf ItemPropertyChanged

        Me.entriesByKey.Remove(item.Key)
        MyBase.RemoveItem(index)
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemDeleted, index))
    End Sub

    Protected Overridable Sub OnListChanged(ByVal e As ListChangedEventArgs)
        RaiseEvent ListChanged(Me, e)
    End Sub

#End Region 'Methods

#Region " IBindingList Support "

#Region " Properties "

    Public ReadOnly Property AllowEdit() As Boolean Implements System.ComponentModel.IBindingList.AllowEdit
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property AllowNew() As Boolean Implements System.ComponentModel.IBindingList.AllowNew
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property AllowRemove() As Boolean Implements System.ComponentModel.IBindingList.AllowRemove
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property IsSorted() As Boolean Implements System.ComponentModel.IBindingList.IsSorted
        Get
            Throw New NotSupportedException("Sorting is not supported.")
        End Get
    End Property

    Public ReadOnly Property SortDirection() As System.ComponentModel.ListSortDirection Implements System.ComponentModel.IBindingList.SortDirection
        Get
            Throw New NotSupportedException("Sorting is not supported.")
        End Get
    End Property

    Public ReadOnly Property SortProperty() As System.ComponentModel.PropertyDescriptor Implements System.ComponentModel.IBindingList.SortProperty
        Get
            Throw New NotSupportedException("Sorting is not supported.")
        End Get
    End Property

    Public ReadOnly Property SupportsChangeNotification() As Boolean Implements System.ComponentModel.IBindingList.SupportsChangeNotification
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property SupportsSearching() As Boolean Implements System.ComponentModel.IBindingList.SupportsSearching
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property SupportsSorting() As Boolean Implements System.ComponentModel.IBindingList.SupportsSorting
        Get
            Return False
        End Get
    End Property

#End Region 'Properties

#Region " Events "

    Public Event ListChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ListChangedEventArgs) Implements System.ComponentModel.IBindingList.ListChanged

#End Region 'Events

#Region " Methods "

    Public Sub AddIndex(ByVal [property] As System.ComponentModel.PropertyDescriptor) Implements System.ComponentModel.IBindingList.AddIndex
        'Do nothing.
    End Sub

    Public Function AddNew() As Object Implements System.ComponentModel.IBindingList.AddNew
        Throw New NotSupportedException("All new items must be added explicitly using the Add method.")
    End Function

    Public Sub ApplySort(ByVal [property] As System.ComponentModel.PropertyDescriptor, ByVal direction As System.ComponentModel.ListSortDirection) Implements System.ComponentModel.IBindingList.ApplySort
        Throw New NotSupportedException("Sorting is not supported.")
    End Sub

    Public Function Find(ByVal [property] As System.ComponentModel.PropertyDescriptor, ByVal key As Object) As Integer Implements System.ComponentModel.IBindingList.Find
        Throw New NotSupportedException("Searching is not supported.")
    End Function

    Public Sub RemoveIndex(ByVal [property] As System.ComponentModel.PropertyDescriptor) Implements System.ComponentModel.IBindingList.RemoveIndex
        'Do nothing.
    End Sub

    Public Sub RemoveSort() Implements System.ComponentModel.IBindingList.RemoveSort
        Throw New NotSupportedException("Sorting is not supported.")
    End Sub

#End Region 'Methods

#End Region 'IBindingList Support

End Class
I implemented the IBindingList interface in that collection for the very same reason you want to: so a bound grid would automatically update whenever I changed the list.

Note that I strongly suggest copying and pasting that code into the IDE. Trying to read it in this post will do your head in.
 
Thankyou for the solution sir - as always very detailed and thorough.

I think ill use the BindingSource wrapper for now - looks like the easiest way to achieve what I want. Ill prob revisit and look at implementing my own BindingLists later.
 
Back
Top